Compare commits

..
Author SHA1 Message Date
rahulramesha 32b4019a69 add Layout options while creating a new view 2024-06-19 16:32:31 +05:30
337 changed files with 1701 additions and 2233 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
push:
branches:
- preview
- develop
env:
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
+2 -2
View File
@@ -1,10 +1,10 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// components
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
+3 -3
View File
@@ -1,14 +1,13 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useTheme } from "next-themes";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// hooks
@@ -17,6 +16,7 @@ import { useInstance } from "@/hooks/store";
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
// local components
import { AuthenticationMethodCard } from "../components";
import { InstanceGithubConfigForm } from "./form";
const InstanceGithubAuthenticationPage = observer(() => {
+3 -3
View File
@@ -1,18 +1,18 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
// local components
import { AuthenticationMethodCard } from "../components";
import { InstanceGitlabConfigForm } from "./form";
const InstanceGitlabAuthenticationPage = observer(() => {
+3 -3
View File
@@ -1,18 +1,18 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import GoogleLogo from "@/public/logos/google-logo.svg";
// local components
import { AuthenticationMethodCard } from "../components";
import { InstanceGoogleConfigForm } from "./form";
const InstanceGoogleAuthenticationPage = observer(() => {
+88 -7
View File
@@ -1,17 +1,41 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useTheme } from "next-themes";
import useSWR from "swr";
import { Mails, KeyRound } from "lucide-react";
import { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
import { PageHeader } from "@/components/common";
// helpers
import { cn } from "@/helpers/common.helper";
// components
import { PageHeader } from "@/components/core";
// hooks
// helpers
import { cn, resolveGeneralTheme } from "@/helpers/common.helper";
import { useInstance } from "@/hooks/store";
// plane admin components
import { AuthenticationModes } from "@/plane-admin/components/authentication";
// images
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
import GoogleLogo from "@/public/logos/google-logo.svg";
// local components
import {
AuthenticationMethodCard,
EmailCodesConfiguration,
PasswordLoginConfiguration,
GitlabConfiguration,
GithubConfiguration,
GoogleConfiguration,
} from "./components";
type TInstanceAuthenticationMethodCard = {
key: string;
name: string;
description: string;
icon: JSX.Element;
config: JSX.Element;
};
const InstanceAuthenticationPage = observer(() => {
// store
@@ -21,6 +45,8 @@ const InstanceAuthenticationPage = observer(() => {
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// theme
const { resolvedTheme } = useTheme();
// derived values
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
@@ -55,6 +81,52 @@ const InstanceAuthenticationPage = observer(() => {
});
};
// Authentication methods
const authenticationMethodsCard: TInstanceAuthenticationMethodCard[] = [
{
key: "email-codes",
name: "Email codes",
description: "Login or sign up using codes sent via emails. You need to have email setup here and enabled.",
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <EmailCodesConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
{
key: "password-login",
name: "Password based login",
description: "Allow members to create accounts with passwords for emails to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <PasswordLoginConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
{
key: "google",
name: "Google",
description: "Allow members to login or sign up to plane with their Google accounts.",
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
{
key: "github",
name: "Github",
description: "Allow members to login or sign up to plane with their Github accounts.",
icon: (
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
),
config: <GithubConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
{
key: "gitlab",
name: "GitLab",
description: "Allow members to login or sign up to plane with their GitLab accounts.",
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
];
return (
<>
<PageHeader title="Authentication - God Mode" />
@@ -96,7 +168,16 @@ const InstanceAuthenticationPage = observer(() => {
</div>
</div>
<div className="text-lg font-medium pt-6">Authentication modes</div>
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
{authenticationMethodsCard.map((method) => (
<AuthenticationMethodCard
key={method.key}
name={method.name}
description={method.description}
icon={method.icon}
config={method.config}
disabled={isSubmitting}
/>
))}
</div>
) : (
<Loader className="space-y-10">
+2 -2
View File
@@ -1,10 +1,10 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// components
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { Telescope } from "lucide-react";
// types
+1 -1
View File
@@ -1,5 +1,5 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks/store";
// components
+2 -2
View File
@@ -1,10 +1,10 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// local
+4 -6
View File
@@ -16,12 +16,10 @@ import { UserProvider } from "@/lib/user-provider";
// styles
import "./globals.css";
const ToastWithTheme = () => {
const { resolvedTheme } = useTheme();
return <Toast theme={resolveGeneralTheme(resolvedTheme)} />;
};
export default function RootLayout({ children }: { children: ReactNode }) {
// themes
const { resolvedTheme } = useTheme();
return (
<html lang="en">
<head>
@@ -33,7 +31,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
</head>
<body className={`antialiased`}>
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<ToastWithTheme />
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
<SWRConfig value={SWR_CONFIG}>
<StoreProvider>
<InstanceProvider>
@@ -1,104 +0,0 @@
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { KeyRound, Mails } from "lucide-react";
// types
import { TInstanceAuthenticationMethodKeys, TInstanceAuthenticationModes } from "@plane/types";
// components
import {
AuthenticationMethodCard,
EmailCodesConfiguration,
GithubConfiguration,
GitlabConfiguration,
GoogleConfiguration,
PasswordLoginConfiguration,
} from "@/components/authentication";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// images
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
import GoogleLogo from "@/public/logos/google-logo.svg";
export type TAuthenticationModeProps = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export type TGetAuthenticationModeProps = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
resolvedTheme: string | undefined;
};
// Authentication methods
export const getAuthenticationModes: (props: TGetAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
disabled,
updateConfig,
resolvedTheme,
}) => [
{
key: "email-codes",
name: "Email codes",
description: "Login or sign up using codes sent via emails. You need to have email setup here and enabled.",
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "password-login",
name: "Password based login",
description: "Allow members to create accounts with passwords for emails to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "google",
name: "Google",
description: "Allow members to login or sign up to plane with their Google accounts.",
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "github",
name: "Github",
description: "Allow members to login or sign up to plane with their Github accounts.",
icon: (
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
),
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "gitlab",
name: "GitLab",
description: "Allow members to login or sign up to plane with their GitLab accounts.",
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
];
export const AuthenticationModes: React.FC<TAuthenticationModeProps> = observer((props) => {
const { disabled, updateConfig } = props;
// next-themes
const { resolvedTheme } = useTheme();
return (
<>
{getAuthenticationModes({ disabled, updateConfig, resolvedTheme }).map((method) => (
<AuthenticationMethodCard
key={method.key}
name={method.name}
description={method.description}
icon={method.icon}
config={method.config}
disabled={disabled}
/>
))}
</>
);
});
@@ -1 +0,0 @@
export * from "./authentication-modes";
@@ -1,7 +1,7 @@
"use client";
import { FC, useState, useRef } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
@@ -1,14 +1,14 @@
"use client";
import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
import { useTheme } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
export interface IInstanceSidebar { }
export interface IInstanceSidebar {}
export const InstanceSidebar: FC<IInstanceSidebar> = observer(() => {
// store
@@ -1,7 +1,7 @@
"use client";
import { Fragment, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import { useTheme as useNextTheme } from "next-themes";
import { LogOut, UserCog2, Palette } from "lucide-react";
import { Menu, Transition } from "@headlessui/react";
@@ -1,7 +1,7 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { Menu } from "lucide-react";
import { useTheme } from "@/hooks/store";
@@ -1,6 +1,6 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
@@ -1,7 +1,7 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import { usePathname } from "next/navigation";
// mobx
// ui
@@ -10,7 +10,7 @@ import { Settings } from "lucide-react";
import { Breadcrumbs } from "@plane/ui";
// components
import { SidebarHamburgerToggle } from "@/components/admin-sidebar";
import { BreadcrumbLink } from "@/components/common";
import { BreadcrumbLink } from "components/common";
export const InstanceHeader: FC = observer(() => {
const pathName = usePathname();
@@ -6,4 +6,4 @@ export * from "./password-strength-meter";
export * from "./banner";
export * from "./empty-state";
export * from "./logo-spinner";
export * from "./page-header";
export * from "./toast";
@@ -0,0 +1,69 @@
"use client";
// helpers
import { CircleCheck } from "lucide-react";
import { cn } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper";
// icons
type Props = {
password: string;
};
export const PasswordStrengthMeter: React.FC<Props> = (props: Props) => {
const { password } = props;
const strength = getPasswordStrength(password);
let bars = [];
let text = "";
let textColor = "";
if (password.length === 0) {
bars = [`bg-[#F0F0F3]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`];
text = "Password requirements";
} else if (password.length < 8) {
bars = [`bg-[#DC3E42]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`];
text = "Password is too short";
textColor = `text-[#DC3E42]`;
} else if (strength < 3) {
bars = [`bg-[#FFBA18]`, `bg-[#FFBA18]`, `bg-[#F0F0F3]`];
text = "Password is weak";
textColor = `text-[#FFBA18]`;
} else {
bars = [`bg-[#3E9B4F]`, `bg-[#3E9B4F]`, `bg-[#3E9B4F]`];
text = "Password is strong";
textColor = `text-[#3E9B4F]`;
}
const criteria = [
{ label: "Min 8 characters", isValid: password.length >= 8 },
{ label: "Min 1 upper-case letter", isValid: /[A-Z]/.test(password) },
{ label: "Min 1 number", isValid: /\d/.test(password) },
{ label: "Min 1 special character", isValid: /[!@#$%^&*]/.test(password) },
];
return (
<div className="w-full">
<div className="flex w-full gap-1.5">
{bars.map((color, index) => (
<div key={index} className={cn("w-full h-1 rounded-full", color)} />
))}
</div>
<p className={cn("text-xs font-medium py-1", textColor)}>{text}</p>
<div className="flex flex-wrap gap-x-4 gap-y-2">
{criteria.map((criterion, index) => (
<div
key={index}
className={cn(
"flex items-center gap-1 text-xs font-medium",
criterion.isValid ? `text-[#3E9B4F]` : "text-custom-text-400"
)}
>
<CircleCheck width={14} height={14} />
{criterion.label}
</div>
))}
</div>
</div>
);
};
+13
View File
@@ -0,0 +1,13 @@
"use client";
import { useTheme } from "next-themes";
// ui
import { Toast as ToastComponent } from "@plane/ui";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
export const Toast = () => {
const { theme } = useTheme();
return <ToastComponent theme={resolveGeneralTheme(theme)} />;
};
+1
View File
@@ -0,0 +1 @@
export * from "./page-header";
@@ -10,7 +10,7 @@ import { Button, Checkbox, Input, Spinner } from "@plane/ui";
import { Banner, PasswordStrengthMeter } from "@/components/common";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper";
import { getPasswordStrength } from "@/helpers/password.helper";
// services
import { AuthService } from "@/services/auth.service";
@@ -121,7 +121,7 @@ export const InstanceSetupForm: FC = (props) => {
formData.first_name &&
formData.email &&
formData.password &&
getPasswordStrength(formData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
getPasswordStrength(formData.password) >= 3 &&
formData.password === formData.confirm_password
? false
: true,
@@ -271,7 +271,7 @@ export const InstanceSetupForm: FC = (props) => {
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
)}
<PasswordStrengthMeter password={formData.password} isFocused={isPasswordInputFocused} />
{isPasswordInputFocused && <PasswordStrengthMeter password={formData.password} />}
</div>
<div className="w-full space-y-1">
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useTheme as nextUseTheme } from "next-themes";
// ui
@@ -1,94 +0,0 @@
"use client";
import { FC, useMemo } from "react";
// import { CircleCheck } from "lucide-react";
// helpers
import { cn } from "@/helpers/common.helper";
import {
E_PASSWORD_STRENGTH,
// PASSWORD_CRITERIA,
getPasswordStrength,
} from "@/helpers/password.helper";
type TPasswordStrengthMeter = {
password: string;
isFocused?: boolean;
};
export const PasswordStrengthMeter: FC<TPasswordStrengthMeter> = (props) => {
const { password, isFocused = false } = props;
// derived values
const strength = useMemo(() => getPasswordStrength(password), [password]);
const strengthBars = useMemo(() => {
switch (strength) {
case E_PASSWORD_STRENGTH.EMPTY: {
return {
bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
text: "Please enter your password.",
textColor: "text-custom-text-100",
};
}
case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID: {
return {
bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`],
text: "Password length should me more than 8 characters.",
textColor: "text-red-500",
};
}
case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID: {
return {
bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`],
text: "Password is weak.",
textColor: "text-red-500",
};
}
case E_PASSWORD_STRENGTH.STRENGTH_VALID: {
return {
bars: [`bg-green-500`, `bg-green-500`, `bg-green-500`],
text: "Password is strong.",
textColor: "text-green-500",
};
}
default: {
return {
bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
text: "Please enter your password.",
textColor: "text-custom-text-100",
};
}
}
}, [strength]);
const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true;
if (!isPasswordMeterVisible) return <></>;
return (
<div className="w-full space-y-2 pt-2">
<div className="space-y-1.5">
<div className="relative flex items-center gap-2">
{strengthBars?.bars.map((color, index) => (
<div key={`${color}-${index}`} className={cn("w-full h-1 rounded-full", color)} />
))}
</div>
<div className={cn(`text-xs font-medium text-custom-text-100`, strengthBars?.textColor)}>
{strengthBars?.text}
</div>
</div>
{/* <div className="relative flex flex-wrap gap-x-4 gap-y-2">
{PASSWORD_CRITERIA.map((criteria) => (
<div
key={criteria.key}
className={cn(
"relative flex items-center gap-1 text-xs",
criteria.isCriteriaValid(password) ? `text-green-500/70` : "text-custom-text-300"
)}
>
<CircleCheck width={14} height={14} />
{criteria.label}
</div>
))}
</div> */}
</div>
);
};
@@ -1 +0,0 @@
export * from "ce/components/authentication/authentication-modes";
@@ -1 +0,0 @@
export * from "./authentication-modes";
+10 -61
View File
@@ -1,67 +1,16 @@
import zxcvbn from "zxcvbn";
export enum E_PASSWORD_STRENGTH {
EMPTY = "empty",
LENGTH_NOT_VALID = "length_not_valid",
STRENGTH_NOT_VALID = "strength_not_valid",
STRENGTH_VALID = "strength_valid",
}
export const isPasswordCriteriaMet = (password: string) => {
const criteria = [password.length >= 8, /[A-Z]/.test(password), /\d/.test(password), /[!@#$%^&*]/.test(password)];
const PASSWORD_MIN_LENGTH = 8;
// const PASSWORD_NUMBER_REGEX = /\d/;
// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/;
// const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/;
return criteria.every((criterion) => criterion);
};
export const PASSWORD_CRITERIA = [
{
key: "min_8_char",
label: "Min 8 characters",
isCriteriaValid: (password: string) => password.length >= PASSWORD_MIN_LENGTH,
},
// {
// key: "min_1_upper_case",
// label: "Min 1 upper-case letter",
// isCriteriaValid: (password: string) => PASSWORD_NUMBER_REGEX.test(password),
// },
// {
// key: "min_1_number",
// label: "Min 1 number",
// isCriteriaValid: (password: string) => PASSWORD_CHAR_CAPS_REGEX.test(password),
// },
// {
// key: "min_1_special_char",
// label: "Min 1 special character",
// isCriteriaValid: (password: string) => PASSWORD_SPECIAL_CHAR_REGEX.test(password),
// },
];
export const getPasswordStrength = (password: string) => {
if (password.length === 0) return 0;
if (password.length < 8) return 1;
if (!isPasswordCriteriaMet(password)) return 2;
export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => {
let passwordStrength: E_PASSWORD_STRENGTH = E_PASSWORD_STRENGTH.EMPTY;
if (!password || password === "" || password.length <= 0) {
return passwordStrength;
}
if (password.length >= PASSWORD_MIN_LENGTH) {
passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID;
} else {
passwordStrength = E_PASSWORD_STRENGTH.LENGTH_NOT_VALID;
return passwordStrength;
}
const passwordCriteriaValidation = PASSWORD_CRITERIA.map((criteria) => criteria.isCriteriaValid(password)).every(
(criterion) => criterion
);
const passwordStrengthScore = zxcvbn(password).score;
if (passwordCriteriaValidation === false || passwordStrengthScore <= 2) {
passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID;
return passwordStrength;
}
if (passwordCriteriaValidation === true && passwordStrengthScore >= 3) {
passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_VALID;
}
return passwordStrength;
const result = zxcvbn(password);
return result.score;
};
@@ -1,6 +1,6 @@
"use client";
import { FC, ReactNode, useEffect } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/navigation";
// components
import { InstanceSidebar } from "@/components/admin-sidebar";
@@ -1,5 +1,5 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common";
@@ -1,7 +1,7 @@
"use client";
import { FC, ReactNode, useEffect } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// hooks
import { useInstance, useTheme, useUser } from "@/hooks/store";
+1 -1
View File
@@ -22,7 +22,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.356.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"mobx-react-lite": "^4.0.5",
"next": "^14.2.3",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
@@ -1,7 +1,7 @@
// helpers
import { API_BASE_URL } from "helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
import { APIService } from "services/api.service";
type TCsrfTokenResponse = {
csrf_token: string;
@@ -1,9 +1,9 @@
// helpers
import { API_BASE_URL } from "helpers/common.helper";
// services
import { APIService } from "services/api.service";
// types
import type { IUser } from "@plane/types";
// services
import { APIService } from "@/services/api.service";
interface IUserSession extends IUser {
isAuthenticated: boolean;
@@ -9,7 +9,7 @@ import {
IInstanceConfig,
} from "@plane/types";
// helpers
import { EInstanceStatus, TInstanceStatus } from "@/helpers/instance.helper";
import { EInstanceStatus, TInstanceStatus } from "@/helpers";
// services
import { InstanceService } from "@/services/instance.service";
// root store
@@ -1,4 +1,4 @@
import { enableStaticRendering } from "mobx-react";
import { enableStaticRendering } from "mobx-react-lite";
// stores
import { IInstanceStore, InstanceStore } from "./instance.store";
import { IThemeStore, ThemeStore } from "./theme.store";
@@ -1,7 +1,7 @@
import { action, observable, runInAction, makeObservable } from "mobx";
import { IUser } from "@plane/types";
// helpers
import { EUserStatus, TUserStatus } from "@/helpers/user.helper";
import { EUserStatus, TUserStatus } from "@/helpers";
// services
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
+1 -4
View File
@@ -7,10 +7,7 @@
"jsx": "preserve",
"esModuleInterop": true,
"paths": {
"@/*": ["core/*"],
"@/helpers/*": ["helpers/*"],
"@/public/*": ["public/*"],
"@/plane-admin/*": ["ce/*"]
"@/*": ["*"]
},
"plugins": [
{
+4 -31
View File
@@ -21,7 +21,6 @@ from plane.db.models import (
ProjectMember,
State,
User,
Project,
)
from .base import BaseSerializer
@@ -29,8 +28,6 @@ from .cycle import CycleLiteSerializer, CycleSerializer
from .module import ModuleLiteSerializer, ModuleSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
from plane.utils.metadata import get_metadata
from plane.utils.presigned_url_generator import generate_download_presigned_url
class IssueSerializer(BaseSerializer):
@@ -275,8 +272,6 @@ class LabelSerializer(BaseSerializer):
class IssueLinkSerializer(BaseSerializer):
metadata = serializers.SerializerMethodField()
class Meta:
model = IssueLink
fields = "__all__"
@@ -291,12 +286,6 @@ class IssueLinkSerializer(BaseSerializer):
"updated_at",
]
def get_metadata(self, obj):
logo = obj.metadata.get("logo", None)
if logo:
obj.metadata["logo"] = generate_download_presigned_url(logo)
return obj.metadata
def validate_url(self, value):
# Check URL format
validate_url = URLValidator()
@@ -320,33 +309,17 @@ class IssueLinkSerializer(BaseSerializer):
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
# Workspace
project = Project.objects.get(pk=validated_data.get("project_id"))
# Fetch metadata from URL
validated_data["metadata"] = get_metadata(
validated_data.get("url"), project.workspace_id
)
return IssueLink.objects.create(**validated_data)
def update(self, instance, validated_data):
if (
IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
).exclude(pk=instance.id).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
validated_data["metadata"] = get_metadata(
validated_data.get("url"), instance.workspace_id
)
return super().update(instance, validated_data)
@@ -37,6 +37,7 @@ from .project import (
)
from .state import StateSerializer, StateLiteSerializer
from .view import (
GlobalViewSerializer,
IssueViewSerializer,
)
from .cycle import (
+4 -30
View File
@@ -33,10 +33,7 @@ from plane.db.models import (
IssueVote,
IssueRelation,
State,
Project,
)
from plane.utils.metadata import get_metadata
from plane.utils.presigned_url_generator import generate_download_presigned_url
class IssueFlatSerializer(BaseSerializer):
@@ -422,7 +419,6 @@ class IssueModuleDetailSerializer(BaseSerializer):
class IssueLinkSerializer(BaseSerializer):
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
metadata = serializers.SerializerMethodField()
class Meta:
model = IssueLink
@@ -437,12 +433,6 @@ class IssueLinkSerializer(BaseSerializer):
"issue",
]
def get_metadata(self, obj):
logo = obj.metadata.get("logo", None)
if logo:
obj.metadata["logo"] = generate_download_presigned_url(logo)
return obj.metadata
def validate_url(self, value):
# Check URL format
validate_url = URLValidator()
@@ -459,7 +449,6 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
# Check if URL already exists for this Issue
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=validated_data.get("issue_id"),
@@ -467,32 +456,17 @@ class IssueLinkSerializer(BaseSerializer):
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
# Workspace
project = Project.objects.get(pk=validated_data.get("project_id"))
# Fetch metadata from URL
validated_data["metadata"] = get_metadata(
validated_data.get("url"), project.workspace_id
)
return IssueLink.objects.create(**validated_data)
def update(self, instance, validated_data):
if (
IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
).exclude(pk=instance.id).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
validated_data["metadata"] = get_metadata(
validated_data.get("url"), instance.workspace_id
)
return super().update(instance, validated_data)
+39 -5
View File
@@ -2,13 +2,50 @@
from rest_framework import serializers
# Module imports
from .base import DynamicBaseSerializer
from plane.db.models import IssueView
from .base import BaseSerializer, DynamicBaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import GlobalView, IssueView
from plane.utils.issue_filters import issue_filters
class GlobalViewSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
class Meta:
model = GlobalView
fields = "__all__"
read_only_fields = [
"workspace",
"query",
]
def create(self, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
return GlobalView.objects.create(**validated_data)
def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
class IssueViewSerializer(DynamicBaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
class Meta:
model = IssueView
@@ -17,9 +54,6 @@ class IssueViewSerializer(DynamicBaseSerializer):
"workspace",
"project",
"query",
"owned_by",
"access",
"is_locked",
]
def create(self, validated_data):
+5 -5
View File
@@ -3,8 +3,8 @@ from django.urls import path
from plane.app.views import (
IssueViewViewSet,
WorkspaceViewViewSet,
WorkspaceViewIssuesViewSet,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewFavoriteViewSet,
)
@@ -34,7 +34,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/views/",
WorkspaceViewViewSet.as_view(
GlobalViewViewSet.as_view(
{
"get": "list",
"post": "create",
@@ -44,7 +44,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/views/<uuid:pk>/",
WorkspaceViewViewSet.as_view(
GlobalViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
@@ -56,7 +56,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/issues/",
WorkspaceViewIssuesViewSet.as_view(
GlobalViewIssuesViewSet.as_view(
{
"get": "list",
}
+2 -2
View File
@@ -80,8 +80,8 @@ from .workspace.cycle import (
from .state.base import StateViewSet
from .view.base import (
WorkspaceViewViewSet,
WorkspaceViewIssuesViewSet,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewViewSet,
IssueViewFavoriteViewSet,
)
+4 -64
View File
@@ -52,7 +52,7 @@ from plane.db.models import (
)
class WorkspaceViewViewSet(BaseViewSet):
class GlobalViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer
model = IssueView
permission_classes = [
@@ -61,7 +61,7 @@ class WorkspaceViewViewSet(BaseViewSet):
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
serializer.save(workspace_id=workspace.id, owned_by=self.request.user)
serializer.save(workspace_id=workspace.id)
def get_queryset(self):
return self.filter_queryset(
@@ -69,42 +69,13 @@ class WorkspaceViewViewSet(BaseViewSet):
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__isnull=True)
.filter(Q(owned_by=self.request.user) | Q(access=1))
.select_related("workspace")
.order_by(self.request.GET.get("order_by", "-created_at"))
.distinct()
)
def partial_update(self, request, slug, pk):
workspace_view = IssueView.objects.get(
pk=pk,
workspace__slug=slug,
)
if workspace_view.is_locked:
return Response(
{"error": "view is locked"},
status=status.HTTP_400_BAD_REQUEST,
)
# Only update the view if owner is updating
if workspace_view.owned_by_id != request.user.id:
return Response(
{"error": "Only the owner of the view can update the view"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = IssueViewSerializer(
workspace_view, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class WorkspaceViewIssuesViewSet(BaseViewSet):
class GlobalViewIssuesViewSet(BaseViewSet):
permission_classes = [
WorkspaceEntityPermission,
]
@@ -300,10 +271,7 @@ class IssueViewViewSet(BaseViewSet):
]
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
owned_by=self.request.user,
)
serializer.save(project_id=self.kwargs.get("project_id"))
def get_queryset(self):
subquery = UserFavorite.objects.filter(
@@ -323,7 +291,6 @@ class IssueViewViewSet(BaseViewSet):
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.filter(Q(owned_by=self.request.user) | Q(access=1))
.select_related("project")
.select_related("workspace")
.annotate(is_favorite=Exists(subquery))
@@ -343,33 +310,6 @@ class IssueViewViewSet(BaseViewSet):
).data
return Response(views, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk):
issue_view = IssueView.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
if issue_view.is_locked:
return Response(
{"error": "view is locked"},
status=status.HTTP_400_BAD_REQUEST,
)
# Only update the view if owner is updating
if issue_view.owned_by_id != request.user.id:
return Response(
{"error": "Only the owner of the view can update the view"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = IssueViewSerializer(
issue_view, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class IssueViewFavoriteViewSet(BaseViewSet):
model = UserFavorite
@@ -2,6 +2,7 @@
from django.db.models import (
Q,
Count,
Sum,
)
# Third party modules
@@ -86,6 +87,29 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
),
)
)
.annotate(
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.order_by(self.kwargs.get("order_by", "-created_at"))
.distinct()
)
+1 -1
View File
@@ -90,7 +90,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug):
# Generate presigned url for the uploaded file with different base
presign_s3 = boto3.client(
"s3",
endpoint_url=f"{settings.AWS_S3_URL_PROTOCOL}//{str(settings.AWS_S3_CUSTOM_DOMAIN).replace(settings.AWS_STORAGE_BUCKET_NAME, '')}/",
endpoint_url=f"{settings.AWS_S3_URL_PROTOCOL}//{str(settings.AWS_S3_CUSTOM_DOMAIN).replace('/uploads', '')}/",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
@@ -1,16 +1,6 @@
# Generated by Django 4.2.11 on 2024-06-03 17:16
from django.db import migrations, models
from django.conf import settings
from django.db.models import F
import django.db.models.deletion
def populate_views_owned_by(apps, schema_editor):
IssueView = apps.get_model("db", "IssueView")
# update all existing views to be owned by the user who created them
IssueView.objects.update(owned_by_id=F("created_by_id"))
class Migration(migrations.Migration):
@@ -21,53 +11,13 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterField(
model_name="account",
name="provider",
field=models.CharField(
choices=[
("google", "Google"),
("github", "Github"),
("gitlab", "GitLab"),
]
),
model_name='account',
name='provider',
field=models.CharField(choices=[('google', 'Google'), ('github', 'Github'), ('gitlab', 'GitLab')]),
),
migrations.AlterField(
model_name="socialloginconnection",
name="medium",
field=models.CharField(
choices=[
("Google", "google"),
("Github", "github"),
("GitLab", "gitlab"),
("Jira", "jira"),
],
default=None,
max_length=20,
),
),
migrations.AddField(
model_name="issueview",
name="is_locked",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="issueview",
name="owned_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="views",
to=settings.AUTH_USER_MODEL,
null=True,
),
),
migrations.RunPython(populate_views_owned_by),
migrations.AlterField(
model_name="issueview",
name="owned_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="views",
to=settings.AUTH_USER_MODEL,
),
model_name='socialloginconnection',
name='medium',
field=models.CharField(choices=[('Google', 'google'), ('Github', 'github'), ('GitLab', 'gitlab'), ('Jira', 'jira')], default=None, max_length=20),
),
]
+1 -1
View File
@@ -65,7 +65,7 @@ from .session import Session
from .social_connection import SocialLoginConnection
from .state import State
from .user import Account, Profile, User
from .view import IssueView, IssueViewFavorite
from .view import GlobalView, IssueView, IssueViewFavorite
from .webhook import Webhook, WebhookLog
from .workspace import (
Team,
-7
View File
@@ -102,13 +102,6 @@ class IssueView(WorkspaceBaseModel):
)
sort_order = models.FloatField(default=65535)
logo_props = models.JSONField(default=dict)
owned_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="views",
)
is_locked = models.BooleanField(default=False)
class Meta:
verbose_name = "Issue View"
-12
View File
@@ -162,8 +162,6 @@ def filter_parent(params, filter, method, prefix=""):
parents = [
item for item in params.get("parent").split(",") if item != "null"
]
if "None" in parents:
filter[f"{prefix}parent__isnull"] = True
parents = filter_valid_uuids(parents)
if len(parents) and "" not in parents:
filter[f"{prefix}parent__in"] = parents
@@ -182,8 +180,6 @@ def filter_labels(params, filter, method, prefix=""):
labels = [
item for item in params.get("labels").split(",") if item != "null"
]
if "None" in labels:
filter[f"{prefix}labels__isnull"] = True
labels = filter_valid_uuids(labels)
if len(labels) and "" not in labels:
filter[f"{prefix}labels__in"] = labels
@@ -204,8 +200,6 @@ def filter_assignees(params, filter, method, prefix=""):
for item in params.get("assignees").split(",")
if item != "null"
]
if "None" in assignees:
filter[f"{prefix}assignees__isnull"] = True
assignees = filter_valid_uuids(assignees)
if len(assignees) and "" not in assignees:
filter[f"{prefix}assignees__in"] = assignees
@@ -248,8 +242,6 @@ def filter_created_by(params, filter, method, prefix=""):
for item in params.get("created_by").split(",")
if item != "null"
]
if "None" in created_bys:
filter[f"{prefix}created_by__isnull"] = True
created_bys = filter_valid_uuids(created_bys)
if len(created_bys) and "" not in created_bys:
filter[f"{prefix}created_by__in"] = created_bys
@@ -393,8 +385,6 @@ def filter_cycle(params, filter, method, prefix=""):
cycles = [
item for item in params.get("cycle").split(",") if item != "null"
]
if "None" in cycles:
filter[f"{prefix}issue_cycle__cycle_id__isnull"] = True
cycles = filter_valid_uuids(cycles)
if len(cycles) and "" not in cycles:
filter[f"{prefix}issue_cycle__cycle_id__in"] = cycles
@@ -413,8 +403,6 @@ def filter_module(params, filter, method, prefix=""):
modules = [
item for item in params.get("module").split(",") if item != "null"
]
if "None" in modules:
filter[f"{prefix}issue_module__module_id__isnull"] = True
modules = filter_valid_uuids(modules)
if len(modules) and "" not in modules:
filter[f"{prefix}issue_module__module_id__in"] = modules
-67
View File
@@ -1,67 +0,0 @@
# Python imports
import requests
import uuid
# Django imports
from django.core.files.base import ContentFile
# Third party imports
from bs4 import BeautifulSoup
import favicon
# Module imports
from plane.db.models import FileAsset
def get_metadata(url, workspace_id):
try:
# Send a GET request to the URL
response = requests.get(url)
response.raise_for_status() # Raise an HTTPError for bad responses
# Parse the HTML content
soup = BeautifulSoup(response.content, "html.parser")
# Extract metadata
metadata = {
"title": soup.title.string if soup.title else "N/A",
"description": "",
"logo": "",
}
# Extract meta tags
meta_tags = soup.find_all("meta")
for tag in meta_tags:
if "name" in tag.attrs:
if tag.attrs["name"].lower() == "description":
metadata["description"] = tag.attrs["content"]
elif tag.attrs["name"].lower() == "keywords":
metadata["keywords"] = tag.attrs["content"]
elif (
"property" in tag.attrs
and tag.attrs["property"].lower() == "og:description"
):
metadata["description"] = tag.attrs["content"]
# Extract favicon
icons = favicon.get(url, timeout=3)
# Download the favicon
if icons:
favicon_response = requests.get(icons[0].url)
content = ContentFile(
favicon_response.content,
name=uuid.uuid4().hex,
)
# Save the favicon as an asset
asset = FileAsset.objects.create(
asset=content,
attributes={"type": "favicon"},
workspace_id=workspace_id,
)
metadata["logo"] = str(asset.asset)
return metadata
except requests.exceptions.RequestException as e:
return {}
@@ -1,47 +0,0 @@
import boto3
from django.conf import settings
from botocore.client import Config
def generate_download_presigned_url(
key,
expiration=3600,
content_type="image/jpeg",
):
"""
Generate a presigned URL to download an object from S3, dynamically setting
the Content-Disposition based on the file metadata.
"""
# Create a new S3 client
if settings.USE_MINIO:
s3_client = boto3.client(
"s3",
endpoint_url=f"{settings.AWS_S3_URL_PROTOCOL}//{str(settings.AWS_S3_CUSTOM_DOMAIN).replace(settings.AWS_STORAGE_BUCKET_NAME, '')}/",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
else:
s3_client = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
region_name=settings.AWS_REGION,
)
try:
# Generate a presigned URL for the object
url = s3_client.generate_presigned_url(
"get_object",
Params={
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Key": key,
"ResponseContentType": content_type,
},
ExpiresIn=expiration,
)
# Return the presigned URL
return url
except Exception:
return ""
+1 -2
View File
@@ -61,5 +61,4 @@ zxcvbn==4.4.28
pytz==2024.1
# jwt
PyJWT==2.8.0
# favicon
favicon==0.7.0
@@ -2,12 +2,14 @@
// @ts-nocheck
import { NodeViewWrapper } from "@tiptap/react";
import { cn } from "src/lib/utils";
import { useRouter } from "next/navigation";
import { IMentionHighlight } from "src/types/mention-suggestion";
import { useEffect, useState } from "react";
// eslint-disable-next-line import/no-anonymous-default-export
export const MentionNodeView = (props) => {
// TODO: move it to web app
const router = useRouter();
const [highlightsState, setHighlightsState] = useState<IMentionHighlight[]>();
useEffect(() => {
@@ -19,20 +21,25 @@ export const MentionNodeView = (props) => {
hightlights();
}, [props.extension.options]);
const handleClick = () => {
if (!props.extension.options.readonly) {
router.push(props.node.attrs.redirect_uri);
}
};
return (
<NodeViewWrapper className="mention-component inline w-fit">
<a
href={props.node.attrs.redirect_uri}
target="_blank"
<span
className={cn("mention rounded bg-custom-primary-100/20 px-1 py-0.5 font-medium text-custom-primary-100", {
"bg-yellow-500/20 text-yellow-500": highlightsState
? highlightsState.includes(props.node.attrs.entity_identifier)
: false,
"cursor-pointer": !props.extension.options.readonly,
})}
onClick={handleClick}
>
@{props.node.attrs.label}
</a>
</span>
</NodeViewWrapper>
);
};
+1 -1
View File
@@ -81,7 +81,7 @@ export type TInboxDuplicateIssueDetails = {
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
snoozed_till: Date | undefined;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
-8
View File
@@ -1,11 +1,3 @@
export type TInstanceAuthenticationModes = {
key: string;
name: string;
description: string;
icon: JSX.Element;
config: JSX.Element;
};
export type TInstanceAuthenticationMethodKeys =
| "ENABLE_SIGNUP"
| "ENABLE_MAGIC_LINK_LOGIN"
+3
View File
@@ -13,6 +13,9 @@ export interface IProjectView {
is_favorite: boolean;
created_by: string;
updated_by: string;
isLocked: boolean | null;
isPrivate: boolean | null;
isPublished: boolean | null;
name: string;
description: string;
filters: IIssueFilterOptions;
+3 -13
View File
@@ -4,10 +4,9 @@ import { ChevronRight } from "lucide-react";
type BreadcrumbsProps = {
children: React.ReactNode;
onBack?: () => void;
isLoading?: boolean;
};
const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps) => {
const Breadcrumbs = ({ children, onBack }: BreadcrumbsProps) => {
const [isSmallScreen, setIsSmallScreen] = React.useState(false);
React.useEffect(() => {
@@ -22,13 +21,6 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps)
const childrenArray = React.Children.toArray(children);
const BreadcrumbItemLoader = (
<div className="flex items-center gap-1 animate-pulse">
<span className="h-5 w-5 bg-custom-background-80 rounded" />
<span className="h-5 w-16 bg-custom-background-80 rounded" />
</div>
);
return (
<div className="flex items-center space-x-2 overflow-hidden">
{!isSmallScreen && (
@@ -41,7 +33,7 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps)
</div>
)}
<div className={`flex items-center gap-2.5 ${isSmallScreen && index > 0 ? "hidden sm:flex" : "flex"}`}>
{isLoading ? BreadcrumbItemLoader : child}
{child}
</div>
</React.Fragment>
))}
@@ -58,9 +50,7 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps)
)}
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
</div>
<div className="flex items-center gap-2.5">
{isLoading ? BreadcrumbItemLoader : childrenArray[childrenArray.length - 1]}
</div>
<div className="flex items-center gap-2.5">{childrenArray[childrenArray.length - 1]}</div>
</>
)}
{isSmallScreen && childrenArray.length === 1 && childrenArray}
+1 -1
View File
@@ -31,7 +31,7 @@ export const DropdownButton: React.FC<IMultiSelectDropdownButton | ISingleSelect
)}
onClick={handleOnClick}
>
{buttonContent ? <>{buttonContent(isOpen)}</> : <span className={cn("", buttonClassName)}>{value}</span>}
{buttonContent ? <>{buttonContent(isOpen, value)}</> : <span className={cn("", buttonClassName)}>{value}</span>}
</button>
</Combobox.Button>
);
+4 -2
View File
@@ -22,6 +22,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
disableSearch,
keyExtractor,
options,
handleClose,
value,
renderItem,
loader,
@@ -46,7 +47,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
options?.map((option) => (
<Combobox.Option
key={keyExtractor(option)}
value={option.data[option.value]}
value={option.value}
className={({ active, selected }) =>
cn(
"flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
@@ -58,6 +59,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
option.className && option.className({ active, selected })
)
}
onClick={handleClose}
>
{({ selected }) => (
<>
@@ -65,7 +67,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
<>{renderItem({ value: option.data[option.value], selected })}</>
) : (
<>
<span className="flex-grow truncate">{value}</span>
<span className="flex-grow truncate">{option.value}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
+5 -3
View File
@@ -24,8 +24,8 @@ export interface IDropdown {
// options props
keyExtractor: (option: TDropdownOption) => string;
optionsContainerClassName?: string;
queryArray: string[];
sortByKey: string;
queryArray?: string[];
sortByKey?: string;
firstItem?: (optionValue: string) => boolean;
renderItem?: ({ value, selected }: { value: string; selected: boolean }) => React.ReactNode;
loader?: React.ReactNode;
@@ -52,7 +52,7 @@ export interface ISingleSelectDropdown extends IDropdown {
export interface IDropdownButton {
isOpen: boolean;
buttonContent?: (isOpen: boolean) => React.ReactNode;
buttonContent?: (isOpen: boolean, value: string | string[] | undefined) => React.ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
handleOnClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
@@ -79,6 +79,8 @@ export interface IDropdownOptions {
inputContainerClassName?: string;
disableSearch?: boolean;
handleClose?: () => void;
keyExtractor: (option: TDropdownOption) => string;
renderItem: (({ value, selected }: { value: string; selected: boolean }) => React.ReactNode) | undefined;
options: TDropdownOption[] | undefined;
+6 -4
View File
@@ -90,10 +90,12 @@ export const MultiSelectDropdown: FC<IMultiSelectDropdown> = (props) => {
const sortedOptions = useMemo(() => {
if (!options) return undefined;
const filteredOptions = (options || []).filter((options) => {
const queryString = queryArray.map((query) => options.data[query]).join(" ");
return queryString.toLowerCase().includes(query.toLowerCase());
});
const filteredOptions = queryArray
? (options || []).filter((options) => {
const queryString = queryArray.map((query) => options.data[query]).join(" ");
return queryString.toLowerCase().includes(query.toLowerCase());
})
: options;
if (disableSorting) return filteredOptions;
+8 -5
View File
@@ -90,12 +90,14 @@ export const Dropdown: FC<ISingleSelectDropdown> = (props) => {
const sortedOptions = useMemo(() => {
if (!options) return undefined;
const filteredOptions = (options || []).filter((options) => {
const queryString = queryArray.map((query) => options.data[query]).join(" ");
return queryString.toLowerCase().includes(query.toLowerCase());
});
const filteredOptions = queryArray
? (options || []).filter((options) => {
const queryString = queryArray.map((query) => options.data[query]).join(" ");
return queryString.toLowerCase().includes(query.toLowerCase());
})
: options;
if (disableSorting) return filteredOptions;
if (disableSorting || !sortByKey) return filteredOptions;
return sortBy(filteredOptions, [
(option) => firstItem && firstItem(option.data[option.value]),
@@ -157,6 +159,7 @@ export const Dropdown: FC<ISingleSelectDropdown> = (props) => {
value={value}
renderItem={renderItem}
loader={loader}
handleClose={handleClose}
/>
</div>
</Combobox.Options>
@@ -112,7 +112,7 @@ export const EmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
}}
/>
</Tab.Panel>
<Tab.Panel className="h-80 w-full relative overflow-hidden overflow-y-auto">
<Tab.Panel className="h-80 w-full">
<LucideIconsList
defaultColor={defaultIconColor}
onChange={(val) => {
+1 -1
View File
@@ -112,7 +112,7 @@ export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
}}
/>
</Tab.Panel>
<Tab.Panel className="h-80 w-full relative overflow-hidden overflow-y-auto">
<Tab.Panel className="h-80 w-full">
<IconsList
defaultColor={defaultIconColor}
onChange={(val) => {

Some files were not shown because too many files have changed in this diff Show More