Compare commits

..
Author SHA1 Message Date
rahulramesha 32b4019a69 add Layout options while creating a new view 2024-06-19 16:32:31 +05:30
720 changed files with 6235 additions and 6450 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
push:
branches:
- preview
- develop
env:
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
+1 -1
View File
@@ -3,7 +3,7 @@ import { Metadata } from "next";
import { AdminLayout } from "@/layouts/admin-layout";
export const metadata: Metadata = {
title: "Artificial Intelligence Settings - Plane Web",
title: "AI Settings - God Mode",
};
export default function AILayout({ children }: { children: ReactNode }) {
+4 -1
View File
@@ -1,8 +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/core";
// hooks
import { useInstance } from "@/hooks/store";
// components
@@ -16,6 +18,7 @@ const InstanceAIPage = observer(() => {
return (
<>
<PageHeader title="Artificial Intelligence - God Mode" />
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
@@ -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";
+4 -4
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(() => {
@@ -63,7 +63,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
};
return (
<>
<PageHeader title="GitHub Authentication - Plane Web" />
<PageHeader title="Authentication - God Mode" />
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
+4 -4
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(() => {
@@ -57,7 +57,7 @@ const InstanceGitlabAuthenticationPage = observer(() => {
};
return (
<>
<PageHeader title="GitLab Authentication - Plane Web" />
<PageHeader title="Authentication - God Mode" />
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
+4 -4
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(() => {
@@ -57,7 +57,7 @@ const InstanceGoogleAuthenticationPage = observer(() => {
};
return (
<>
<PageHeader title="Google Authentication - Plane Web" />
<PageHeader title="Authentication - God Mode" />
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
+1 -1
View File
@@ -3,7 +3,7 @@ import { Metadata } from "next";
import { AdminLayout } from "@/layouts/admin-layout";
export const metadata: Metadata = {
title: "Authentication Settings - Plane Web",
title: "Authentication Settings - God Mode",
};
export default function AuthenticationLayout({ children }: { children: ReactNode }) {
+96 -10
View File
@@ -1,16 +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";
// 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
@@ -20,6 +45,8 @@ const InstanceAuthenticationPage = observer(() => {
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// theme
const { resolvedTheme } = useTheme();
// derived values
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
@@ -33,7 +60,7 @@ const InstanceAuthenticationPage = observer(() => {
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving configuration",
loading: "Saving Configuration...",
success: {
title: "Success",
message: () => "Configuration saved successfully",
@@ -54,11 +81,58 @@ 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" />
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Manage authentication modes for your instance</div>
<div className="text-xl font-medium text-custom-text-100">Manage authentication for your instance</div>
<div className="text-sm font-normal text-custom-text-300">
Configure authentication modes for your team and restrict sign ups to be invite only.
</div>
@@ -66,12 +140,15 @@ const InstanceAuthenticationPage = observer(() => {
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<div className="space-y-3">
<div className="text-lg font-medium pb-1">Sign-up configuration</div>
<div className={cn("w-full flex items-center gap-14 rounded")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="text-lg font-medium pb-1">Allow anyone to sign up even without an invite</div>
<div className={cn("font-medium leading-5 text-custom-text-100 text-sm")}>
Allow anyone to sign up without invite
</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
Toggling this off will only let users sign up when they are invited.
Toggling this off will disable self sign ups.
</div>
</div>
</div>
@@ -91,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">
+1 -1
View File
@@ -7,7 +7,7 @@ interface EmailLayoutProps {
}
export const metadata: Metadata = {
title: "Email Settings - Plane Web",
title: "Email Settings - God Mode",
};
export default function EmailLayout({ children }: EmailLayoutProps) {
+4 -1
View File
@@ -1,8 +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/core";
// hooks
import { useInstance } from "@/hooks/store";
// components
@@ -16,6 +18,7 @@ const InstanceEmailPage = observer(() => {
return (
<>
<PageHeader title="Email - God Mode" />
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
+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
@@ -3,7 +3,7 @@ import { Metadata } from "next";
import { AdminLayout } from "@/layouts/admin-layout";
export const metadata: Metadata = {
title: "General Settings - Plane Web",
title: "General Settings - God Mode",
};
export default function GeneralLayout({ children }: { children: ReactNode }) {
+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
+1 -1
View File
@@ -7,7 +7,7 @@ interface ImageLayoutProps {
}
export const metadata: Metadata = {
title: "Images Settings - Plane Web",
title: "Images Settings - God Mode",
};
export default function ImageLayout({ children }: ImageLayoutProps) {
+4 -1
View File
@@ -1,8 +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/core";
// hooks
import { useInstance } from "@/hooks/store";
// local
@@ -16,6 +18,7 @@ const InstanceImagePage = observer(() => {
return (
<>
<PageHeader title="Image - God Mode" />
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Third-party image libraries</div>
+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: "unique-codes",
name: "Unique codes",
description: "Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "passwords-login",
name: "Passwords",
description: "Allow members to create accounts with passwords and use it with their email addresses 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 log in or sign up for 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 log in or sign up for 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,11 +1,11 @@
"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 {}
@@ -41,10 +41,10 @@ export const InstanceSidebar: FC<IInstanceSidebar> = observer(() => {
<div
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
fixed md:relative
${isSidebarCollapsed ? "-ml-[280px]" : ""}
sm:${isSidebarCollapsed ? "-ml-[280px]" : ""}
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[280px]"}
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[280px]"}
${isSidebarCollapsed ? "-ml-[250px]" : ""}
sm:${isSidebarCollapsed ? "-ml-[250px]" : ""}
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[250px]"}
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[250px]"}
`}
>
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
@@ -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": [
{
+56 -177
View File
@@ -3,18 +3,9 @@ import json
# Django imports
from django.core import serializers
from django.db.models import Count, F, Func, OuterRef, Q, Sum
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import (
Count,
F,
Func,
OuterRef,
Q,
Sum,
IntegerField,
)
from django.db.models.functions import Cast
# Third party imports
from rest_framework import status
@@ -31,7 +22,6 @@ from plane.db.models import (
Cycle,
CycleIssue,
Issue,
Project,
IssueAttachment,
IssueLink,
)
@@ -125,6 +115,29 @@ class CycleAPIEndpoint(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()
)
@@ -771,6 +784,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
def post(self, request, slug, project_id, cycle_id):
new_cycle_id = request.data.get("new_cycle_id", False)
plot_type = request.GET.get("plot_type", "issues")
if not new_cycle_id:
return Response(
@@ -778,9 +792,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
new_cycle = Cycle.objects.filter(
new_cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
).first()
)
old_cycle = (
Cycle.objects.filter(
@@ -847,131 +861,18 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
)
estimate_type = Project.objects.filter(
workspace__slug=slug,
pk=project_id,
estimate__isnull=False,
estimate__type="points",
).exists()
if estimate_type:
assignee_estimate_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
# assignee distribution serialization
assignee_estimate_distribution = [
{
"display_name": item["display_name"],
"assignee_id": (
str(item["assignee_id"])
if item["assignee_id"]
else None
),
"avatar": item["avatar"],
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
}
for item in assignee_estimate_data
]
label_distribution_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
)
.annotate(label_name=F("labels__name"))
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
estimate_completion_chart = burndown_plot(
queryset=old_cycle.first(),
slug=slug,
project_id=project_id,
plot_type="points",
cycle_id=cycle_id,
)
# Label distribution serialization
label_estimate_distribution = [
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
}
for item in label_distribution_data
]
# Pass the new_cycle queryset to burndown_plot
completion_chart = burndown_plot(
queryset=old_cycle.first(),
slug=slug,
project_id=project_id,
plot_type=plot_type,
cycle_id=cycle_id,
)
# Get the assignee distribution
assignee_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
@@ -983,10 +884,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
@@ -1028,7 +926,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
# Get the label distribution
label_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
@@ -1040,11 +938,8 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate(
completed_issues=Count(
@@ -1084,42 +979,26 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
for item in label_distribution
]
# Pass the new_cycle queryset to burndown_plot
completion_chart = burndown_plot(
queryset=old_cycle.first(),
slug=slug,
project_id=project_id,
plot_type="issues",
cycle_id=cycle_id,
)
current_cycle = Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, pk=cycle_id
).first()
current_cycle.progress_snapshot = {
"total_issues": old_cycle.first().total_issues,
"completed_issues": old_cycle.first().completed_issues,
"cancelled_issues": old_cycle.first().cancelled_issues,
"started_issues": old_cycle.first().started_issues,
"unstarted_issues": old_cycle.first().unstarted_issues,
"backlog_issues": old_cycle.first().backlog_issues,
"distribution": {
"labels": label_distribution_data,
"assignees": assignee_distribution_data,
"completion_chart": completion_chart,
},
"estimate_distribution": (
{}
if not estimate_type
else {
"labels": label_estimate_distribution,
"assignees": assignee_estimate_distribution,
"completion_chart": estimate_completion_chart,
}
),
}
current_cycle.save(update_fields=["progress_snapshot"])
if current_cycle:
current_cycle.progress_snapshot = {
"total_issues": old_cycle.first().total_issues,
"completed_issues": old_cycle.first().completed_issues,
"cancelled_issues": old_cycle.first().cancelled_issues,
"started_issues": old_cycle.first().started_issues,
"unstarted_issues": old_cycle.first().unstarted_issues,
"backlog_issues": old_cycle.first().backlog_issues,
"distribution": {
"labels": label_distribution_data,
"assignees": assignee_distribution_data,
"completion_chart": completion_chart,
},
}
# Save the snapshot of the current cycle
current_cycle.save(update_fields=["progress_snapshot"])
if (
new_cycle.end_date is not None
@@ -37,6 +37,7 @@ from .project import (
)
from .state import StateSerializer, StateLiteSerializer
from .view import (
GlobalViewSerializer,
IssueViewSerializer,
)
from .cycle import (
+20 -33
View File
@@ -44,16 +44,12 @@ class ProjectSerializer(BaseSerializer):
detail="Project Identifier is taken"
)
project = Project.objects.create(
**validated_data,
workspace_id=self.context["workspace_id"],
**validated_data, workspace_id=self.context["workspace_id"]
)
_ = ProjectIdentifier.objects.create(
name=project.identifier,
project=project,
workspace_id=self.context["workspace_id"],
created_by_id=project.created_by_id,
updated_by_id=project.updated_by_id,
current_active=project.identifier,
)
return project
@@ -65,38 +61,29 @@ class ProjectSerializer(BaseSerializer):
project = super().update(instance, validated_data)
return project
# When updating check if the project identifier is different
if instance.identifier == identifier:
# If no Project Identifier is found create it
project_identifier = ProjectIdentifier.objects.filter(
name=identifier, workspace_id=instance.workspace_id
).first()
if project_identifier is None:
project = super().update(instance, validated_data)
project_identifier = ProjectIdentifier.objects.filter(
project=project
).first()
if project_identifier is not None:
project_identifier.name = identifier
project_identifier.save()
return project
# If found check if the project_id to be updated and identifier project id is same
if project_identifier.project_id == instance.id:
# If same pass update
project = super().update(instance, validated_data)
return project
# Check if this project identifier is already taken in the current workspace
if ProjectIdentifier.objects.filter(
name=identifier,
workspace_id=instance.workspace_id,
).exists():
raise serializers.ValidationError(
detail="Project Identifier is already taken"
)
# Update the project identifier
project = super().update(instance, validated_data)
# If no Project Identifier is found create it and deactivate the old ones
ProjectIdentifier.objects.filter(
project_id=instance.id,
workspace_id=instance.workspace_id,
).update(current_active=identifier)
# Create new project identifier
ProjectIdentifier.objects.create(
name=identifier,
project=instance,
workspace_id=instance.workspace_id,
created_by_id=project.created_by_id,
updated_by_id=project.updated_by_id,
current_active=identifier,
# If not same fail update
raise serializers.ValidationError(
detail="Project Identifier is already taken"
)
# Return the updated project
return project
class ProjectLiteSerializer(BaseSerializer):
+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):
+12 -12
View File
@@ -14,7 +14,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/cycles/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
CycleViewSet.as_view(
{
"get": "list",
@@ -24,7 +24,7 @@ urlpatterns = [
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/",
CycleViewSet.as_view(
{
"get": "retrieve",
@@ -36,7 +36,7 @@ urlpatterns = [
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
CycleIssueViewSet.as_view(
{
"get": "list",
@@ -46,7 +46,7 @@ urlpatterns = [
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
CycleIssueViewSet.as_view(
{
"get": "retrieve",
@@ -58,12 +58,12 @@ urlpatterns = [
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/cycles/date-check/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/date-check/",
CycleDateCheckEndpoint.as_view(),
name="project-cycle-date",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-cycles/",
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view(
{
"get": "list",
@@ -73,7 +73,7 @@ urlpatterns = [
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
CycleFavoriteViewSet.as_view(
{
"delete": "destroy",
@@ -82,27 +82,27 @@ urlpatterns = [
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
TransferCycleIssueEndpoint.as_view(),
name="transfer-issues",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/user-properties/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
CycleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/archive/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/archive/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/archived-cycles/",
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/archived-cycles/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/<uuid:pk>/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
+5 -5
View File
@@ -10,12 +10,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/project-estimates/",
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
ProjectEstimatePointEndpoint.as_view(),
name="project-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/estimates/",
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
@@ -25,7 +25,7 @@ urlpatterns = [
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/estimates/<uuid:estimate_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
@@ -36,7 +36,7 @@ urlpatterns = [
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
EstimatePointEndpoint.as_view(
{
"post": "create",
@@ -45,7 +45,7 @@ urlpatterns = [
name="estimate-points",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
EstimatePointEndpoint.as_view(
{
"patch": "partial_update",
+3 -6
View File
@@ -2,10 +2,7 @@ from django.urls import path
from plane.app.views import UnsplashEndpoint
from plane.app.views import (
GPTIntegrationEndpoint,
WorkspaceGPTIntegrationEndpoint,
)
from plane.app.views import GPTIntegrationEndpoint, WorkspaceGPTIntegrationEndpoint
urlpatterns = [
@@ -15,11 +12,11 @@ urlpatterns = [
name="unsplash",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/ai-assistant/",
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
path(
path(
"workspaces/<str:slug>/ai-assistant/",
WorkspaceGPTIntegrationEndpoint.as_view(),
name="importer",
+4 -4
View File
@@ -9,7 +9,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/inboxes/",
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
InboxViewSet.as_view(
{
"get": "list",
@@ -19,7 +19,7 @@ urlpatterns = [
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/inboxes/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
InboxViewSet.as_view(
{
"get": "retrieve",
@@ -30,7 +30,7 @@ urlpatterns = [
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/inbox-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
InboxIssueViewSet.as_view(
{
"get": "list",
@@ -40,7 +40,7 @@ urlpatterns = [
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/inbox-issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
InboxIssueViewSet.as_view(
{
"get": "retrieve",
+31 -31
View File
@@ -25,12 +25,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/list/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/list/",
IssueListEndpoint.as_view(),
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueViewSet.as_view(
{
"get": "list",
@@ -40,7 +40,7 @@ urlpatterns = [
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
IssueViewSet.as_view(
{
"get": "retrieve",
@@ -52,7 +52,7 @@ urlpatterns = [
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issue-labels/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
LabelViewSet.as_view(
{
"get": "list",
@@ -62,7 +62,7 @@ urlpatterns = [
name="project-issue-labels",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issue-labels/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/<uuid:pk>/",
LabelViewSet.as_view(
{
"get": "retrieve",
@@ -74,28 +74,28 @@ urlpatterns = [
name="project-issue-labels",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/bulk-create-labels/",
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-create-labels/",
BulkCreateIssueLabelsEndpoint.as_view(),
name="project-bulk-labels",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/bulk-delete-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-delete-issues/",
BulkDeleteIssuesEndpoint.as_view(),
name="project-issues-bulk",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/bulk-archive-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-archive-issues/",
BulkArchiveIssuesEndpoint.as_view(),
name="bulk-archive-issues",
),
##
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/sub-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
SubIssuesEndpoint.as_view(),
name="sub-issues",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-links/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
IssueLinkViewSet.as_view(
{
"get": "list",
@@ -105,7 +105,7 @@ urlpatterns = [
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-links/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/<uuid:pk>/",
IssueLinkViewSet.as_view(
{
"get": "retrieve",
@@ -117,12 +117,12 @@ urlpatterns = [
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-attachments/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-attachments/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
@@ -134,14 +134,14 @@ urlpatterns = [
## End Issues
## Issue Activity
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/history/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/history/",
IssueActivityEndpoint.as_view(),
name="project-issue-history",
),
## Issue Activity
## IssueComments
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/comments/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentViewSet.as_view(
{
"get": "list",
@@ -151,7 +151,7 @@ urlpatterns = [
name="project-issue-comment",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/comments/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
IssueCommentViewSet.as_view(
{
"get": "retrieve",
@@ -165,7 +165,7 @@ urlpatterns = [
## End IssueComments
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-subscribers/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view(
{
"get": "list",
@@ -175,12 +175,12 @@ urlpatterns = [
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
IssueSubscriberViewSet.as_view({"delete": "destroy"}),
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/subscribe/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{
"get": "subscription_status",
@@ -193,7 +193,7 @@ urlpatterns = [
## End Issue Subscribers
# Issue Reactions
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/reactions/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionViewSet.as_view(
{
"get": "list",
@@ -203,7 +203,7 @@ urlpatterns = [
name="project-issue-reactions",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/reactions/<str:reaction_code>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionViewSet.as_view(
{
"delete": "destroy",
@@ -214,7 +214,7 @@ urlpatterns = [
## End Issue Reactions
# Comment Reactions
path(
"workspaces/<str:slug>/projects/<str:project_id>/comments/<uuid:comment_id>/reactions/",
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionViewSet.as_view(
{
"get": "list",
@@ -224,7 +224,7 @@ urlpatterns = [
name="project-issue-comment-reactions",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionViewSet.as_view(
{
"delete": "destroy",
@@ -235,14 +235,14 @@ urlpatterns = [
## End Comment Reactions
## IssueProperty
path(
"workspaces/<str:slug>/projects/<str:project_id>/user-properties/",
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
## IssueProperty End
## Issue Archives
path(
"workspaces/<str:slug>/projects/<str:project_id>/archived-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
IssueArchiveViewSet.as_view(
{
"get": "list",
@@ -251,7 +251,7 @@ urlpatterns = [
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<uuid:pk>/archive/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
IssueArchiveViewSet.as_view(
{
"get": "retrieve",
@@ -264,7 +264,7 @@ urlpatterns = [
## End Issue Archives
## Issue Relation
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-relation/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"get": "list",
@@ -274,7 +274,7 @@ urlpatterns = [
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/remove-relation/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
IssueRelationViewSet.as_view(
{
"post": "remove_relation",
@@ -285,7 +285,7 @@ urlpatterns = [
## End Issue Relation
## Issue Drafts
path(
"workspaces/<str:slug>/projects/<str:project_id>/issue-drafts/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
IssueDraftViewSet.as_view(
{
"get": "list",
@@ -295,7 +295,7 @@ urlpatterns = [
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issue-drafts/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
IssueDraftViewSet.as_view(
{
"get": "retrieve",
@@ -306,7 +306,7 @@ urlpatterns = [
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/bulk-operation-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-operation-issues/",
BulkIssueOperationsEndpoint.as_view(),
name="bulk-operations-issues",
),
+13 -13
View File
@@ -13,7 +13,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/modules/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/",
ModuleViewSet.as_view(
{
"get": "list",
@@ -23,7 +23,7 @@ urlpatterns = [
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:pk>/",
ModuleViewSet.as_view(
{
"get": "retrieve",
@@ -35,7 +35,7 @@ urlpatterns = [
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/issues/<uuid:issue_id>/modules/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
ModuleIssueViewSet.as_view(
{
"post": "create_issue_modules",
@@ -44,7 +44,7 @@ urlpatterns = [
name="issue-module",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
ModuleIssueViewSet.as_view(
{
"post": "create_module_issues",
@@ -54,7 +54,7 @@ urlpatterns = [
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
ModuleIssueViewSet.as_view(
{
"get": "retrieve",
@@ -66,7 +66,7 @@ urlpatterns = [
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/module-links/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
ModuleLinkViewSet.as_view(
{
"get": "list",
@@ -76,7 +76,7 @@ urlpatterns = [
name="project-issue-module-links",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/module-links/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/<uuid:pk>/",
ModuleLinkViewSet.as_view(
{
"get": "retrieve",
@@ -88,7 +88,7 @@ urlpatterns = [
name="project-issue-module-links",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-modules/",
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
ModuleFavoriteViewSet.as_view(
{
"get": "list",
@@ -98,7 +98,7 @@ urlpatterns = [
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-modules/<uuid:module_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
ModuleFavoriteViewSet.as_view(
{
"delete": "destroy",
@@ -107,22 +107,22 @@ urlpatterns = [
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/user-properties/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
ModuleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/archive/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/archive/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/archived-modules/",
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/archived-modules/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/<uuid:pk>/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
+9 -9
View File
@@ -12,7 +12,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/pages/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
@@ -22,7 +22,7 @@ urlpatterns = [
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
@@ -34,7 +34,7 @@ urlpatterns = [
),
# favorite pages
path(
"workspaces/<str:slug>/projects/<str:project_id>/favorite-pages/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/favorite-pages/<uuid:pk>/",
PageFavoriteViewSet.as_view(
{
"post": "create",
@@ -45,7 +45,7 @@ urlpatterns = [
),
# archived pages
path(
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/archive/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
PageViewSet.as_view(
{
"post": "archive",
@@ -56,7 +56,7 @@ urlpatterns = [
),
# lock and unlock
path(
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/lock/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/lock/",
PageViewSet.as_view(
{
"post": "lock",
@@ -66,22 +66,22 @@ urlpatterns = [
name="project-pages-lock-unlock",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/transactions/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/transactions/<uuid:transaction>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/sub-pages/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/sub-pages/",
SubPagesEndpoint.as_view(),
name="sub-page",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/description/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
PagesDescriptionViewSet.as_view(
{
"get": "retrieve",
+14 -14
View File
@@ -30,7 +30,7 @@ urlpatterns = [
name="project",
),
path(
"workspaces/<str:slug>/projects/<str:pk>/",
"workspaces/<str:slug>/projects/<uuid:pk>/",
ProjectViewSet.as_view(
{
"get": "retrieve",
@@ -47,7 +47,7 @@ urlpatterns = [
name="project-identifiers",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/invitations/",
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
ProjectInvitationsViewset.as_view(
{
"get": "list",
@@ -57,7 +57,7 @@ urlpatterns = [
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/invitations/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
ProjectInvitationsViewset.as_view(
{
"get": "retrieve",
@@ -82,12 +82,12 @@ urlpatterns = [
name="user-project-roles",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/join/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/join/<uuid:pk>/",
ProjectJoinEndpoint.as_view(),
name="project-join",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/members/",
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
ProjectMemberViewSet.as_view(
{
"get": "list",
@@ -97,7 +97,7 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/members/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
ProjectMemberViewSet.as_view(
{
"get": "retrieve",
@@ -108,7 +108,7 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/members/leave/",
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
ProjectMemberViewSet.as_view(
{
"post": "leave",
@@ -117,17 +117,17 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/team-invite/",
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
AddTeamToProjectEndpoint.as_view(),
name="projects",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/project-views/",
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(),
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/project-members/me/",
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/me/",
ProjectMemberUserEndpoint.as_view(),
name="project-member-view",
),
@@ -142,7 +142,7 @@ urlpatterns = [
name="project-favorite",
),
path(
"workspaces/<str:slug>/user-favorite-projects/<str:project_id>/",
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
ProjectFavoritesViewSet.as_view(
{
"delete": "destroy",
@@ -156,7 +156,7 @@ urlpatterns = [
name="project-covers",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/project-deploy-boards/",
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
DeployBoardViewSet.as_view(
{
"get": "list",
@@ -166,7 +166,7 @@ urlpatterns = [
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/project-deploy-boards/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
DeployBoardViewSet.as_view(
{
"get": "retrieve",
@@ -177,7 +177,7 @@ urlpatterns = [
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/archive/",
"workspaces/<str:slug>/projects/<uuid:project_id>/archive/",
ProjectArchiveUnarchiveEndpoint.as_view(),
name="project-archive-unarchive",
),
+1 -1
View File
@@ -14,7 +14,7 @@ urlpatterns = [
name="global-search",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/search-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/search-issues/",
IssueSearchEndpoint.as_view(),
name="project-issue-search",
),
+3 -3
View File
@@ -6,7 +6,7 @@ from plane.app.views import StateViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/states/",
"workspaces/<str:slug>/projects/<uuid:project_id>/states/",
StateViewSet.as_view(
{
"get": "list",
@@ -16,7 +16,7 @@ urlpatterns = [
name="project-states",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/states/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
StateViewSet.as_view(
{
"get": "retrieve",
@@ -27,7 +27,7 @@ urlpatterns = [
name="project-state",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/states/<uuid:pk>/mark-default/",
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/mark-default/",
StateViewSet.as_view(
{
"post": "mark_as_default",
+9 -9
View File
@@ -3,15 +3,15 @@ from django.urls import path
from plane.app.views import (
IssueViewViewSet,
WorkspaceViewViewSet,
WorkspaceViewIssuesViewSet,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewFavoriteViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/views/",
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
IssueViewViewSet.as_view(
{
"get": "list",
@@ -21,7 +21,7 @@ urlpatterns = [
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/views/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
IssueViewViewSet.as_view(
{
"get": "retrieve",
@@ -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",
}
@@ -64,7 +64,7 @@ urlpatterns = [
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-views/",
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
@@ -74,7 +74,7 @@ urlpatterns = [
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-views/<uuid:view_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",
+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 -59
View File
@@ -1,12 +1,12 @@
# Python imports
import traceback
import uuid
import zoneinfo
# Django imports
import zoneinfo
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import IntegrityError
# Django imports
from django.urls import resolve
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
@@ -24,8 +24,6 @@ from rest_framework.viewsets import ModelViewSet
from plane.authentication.session import BaseSessionAuthentication
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
from plane.db.models import ProjectIdentifier, IssueSequence
from plane.utils.valid_int_uuid import is_uuid, is_int_or_uuid
class TimezoneMixin:
@@ -62,44 +60,6 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
search_fields = []
def get_project_id(self, project_id):
# If the project_id is present and is not a UUID then get the project_id from the project_identifier
if project_id and not is_uuid(project_id):
project_identifier = (
ProjectIdentifier.objects.filter(
workspace__slug=self.workspace_slug, name=project_id
)
.values("project_id")
.first()
)
# If the project_identifier is not present then raise ObjectDoesNotExist
if not project_identifier:
raise ObjectDoesNotExist
# Update the project_id in the kwargs
return project_identifier.get("project_id")
return project_id
def get_issue_id(self, issue_id, project_id):
# If the issue_id is present and is not a UUID then get the issue_id from the issue_identifier
if issue_id:
type, value = is_int_or_uuid(issue_id)
if type == "int":
issue_identifier = (
IssueSequence.objects.filter(
project_id=project_id, sequence=value
)
.values("issue_id")
.first()
)
# If the issue_identifier is not present then raise ObjectDoesNotExist
if not issue_identifier:
raise ObjectDoesNotExist
# Update the issue_id in the kwargs
return issue_identifier.get("issue_id")
return issue_id
def get_queryset(self):
try:
return self.model.objects.all()
@@ -156,22 +116,8 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
def dispatch(self, request, *args, **kwargs):
try:
# Write identifier to the uuid
project_id = kwargs.get("project_id", None)
if project_id:
kwargs["project_id"] = self.get_project_id(project_id)
# Check issue_id is present in the kwargs
issue_id = kwargs.get("issue_id", None)
if issue_id:
kwargs["issue_id"] = self.get_issue_id(
issue_id, kwargs["project_id"]
)
# If the project_id is not present then get the project_id from the project_identifier
response = super().dispatch(request, *args, **kwargs)
# Print the number of queries if the debug is enabled
if settings.DEBUG:
from django.db import connection
@@ -179,7 +125,6 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
)
# Return the response
return response
except Exception as exc:
response = self.handle_exception(exc)
@@ -193,7 +138,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
def project_id(self):
project_id = self.kwargs.get("project_id", None)
if project_id:
return self.get_project_id(project_id)
return project_id
if resolve(self.request.path_info).url_name == "project":
return self.kwargs.get("pk", None)
+4 -3
View File
@@ -177,6 +177,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
)
def get(self, request, slug, project_id, pk=None):
plot_type = request.GET.get("plot_type", "issues")
if pk is None:
queryset = (
self.get_queryset()
@@ -278,7 +279,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
# Assignee Distribution
assignee_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_cycle__cycle_id=pk,
workspace__slug=slug,
project_id=project_id,
@@ -326,7 +327,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
# Label Distribution
label_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_cycle__cycle_id=pk,
workspace__slug=slug,
project_id=project_id,
@@ -375,7 +376,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
queryset=queryset,
slug=slug,
project_id=project_id,
plot_type="issues",
plot_type=plot_type,
cycle_id=pk,
)
+4 -4
View File
@@ -1223,7 +1223,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
if estimate_type:
assignee_estimate_data = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
@@ -1277,7 +1277,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
]
label_distribution_data = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
@@ -1338,7 +1338,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
# Get the assignee distribution
assignee_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
@@ -1395,7 +1395,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
# Get the label distribution
label_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
+54 -84
View File
@@ -44,7 +44,6 @@ from plane.db.models import (
IssueReaction,
IssueSubscriber,
Project,
IssueSequence,
)
from plane.utils.grouper import (
issue_group_values,
@@ -59,7 +58,8 @@ from plane.utils.paginator import (
)
from .. import BaseAPIView, BaseViewSet
from plane.utils.user_timezone_converter import user_timezone_converter
from plane.utils.valid_int_uuid import is_int_or_uuid
# Module imports
class IssueListEndpoint(BaseAPIView):
@@ -197,23 +197,6 @@ class IssueViewSet(BaseViewSet):
"workspace__id",
]
def process_issue_id(self, issue_id, project_id):
# Check if the pk is an integer or a UUID
type, value = is_int_or_uuid(issue_id)
if type == "int":
# Get the issue_id from the sequence_id
sequence = (
IssueSequence.objects.filter(
sequence=value,
project_id=project_id,
)
.values("issue_id")
.first()
)
# Call the parent dispatch method
return sequence.get("issue_id")
return issue_id
def get_queryset(self):
return (
Issue.issue_objects.filter(
@@ -427,76 +410,67 @@ class IssueViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk=None):
pk = self.process_issue_id(pk, project_id)
issue = (
(
self.get_queryset()
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
self.get_queryset()
.filter(pk=pk)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
)
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
)
)
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related("issue"),
)
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related(
"issue"
),
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related(
"created_by"
),
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
)
)
)
.filter(pk=pk)
.first()
)
).first()
if not issue:
return Response(
{"error": "The required object does not exist."},
@@ -507,7 +481,6 @@ class IssueViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk=None):
pk = self.process_issue_id(pk, project_id)
issue = self.get_queryset().filter(pk=pk).first()
if not issue:
@@ -542,11 +515,8 @@ class IssueViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, pk=None):
pk = self.process_issue_id(pk, project_id)
issue = Issue.objects.get(
workspace__slug=slug,
project_id=project_id,
pk=pk,
workspace__slug=slug, project_id=project_id, pk=pk
)
issue.delete()
issue_activity.delay(
+4 -4
View File
@@ -233,7 +233,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
if estimate_type:
label_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_module__module_id=pk,
workspace__slug=slug,
project_id=project_id,
@@ -279,7 +279,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
)
assignee_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_module__module_id=pk,
workspace__slug=slug,
project_id=project_id,
@@ -329,7 +329,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
)
)
assignee_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_module__module_id=pk,
workspace__slug=slug,
project_id=project_id,
@@ -379,7 +379,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
)
label_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_module__module_id=pk,
workspace__slug=slug,
project_id=project_id,
+2 -2
View File
@@ -456,7 +456,7 @@ class ModuleViewSet(BaseViewSet):
if estimate_type:
assignee_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_module__module_id=pk,
workspace__slug=slug,
project_id=project_id,
@@ -502,7 +502,7 @@ class ModuleViewSet(BaseViewSet):
)
label_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
issue_module__module_id=pk,
workspace__slug=slug,
project_id=project_id,
+4 -24
View File
@@ -431,12 +431,8 @@ class PagesDescriptionViewSet(BaseViewSet):
]
def retrieve(self, request, slug, project_id, pk):
page = (
Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id
)
.filter(Q(owned_by=self.request.user) | Q(access=0))
.first()
page = Page.objects.get(
pk=pk, workspace__slug=slug, projects__id=project_id
)
binary_data = page.description_binary
@@ -455,26 +451,10 @@ class PagesDescriptionViewSet(BaseViewSet):
return response
def partial_update(self, request, slug, project_id, pk):
page = (
Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id
)
.filter(Q(owned_by=self.request.user) | Q(access=0))
.first()
page = Page.objects.get(
pk=pk, workspace__slug=slug, projects__id=project_id
)
if page.is_locked:
return Response(
{"error": "Page is locked"},
status=471,
)
if page.archived_at:
return Response(
{"error": "Page is archived"},
status=472,
)
base64_data = request.data.get("description_binary")
if base64_data:

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