Compare commits

..
Author SHA1 Message Date
pablohashescobar bdffb58581 dev: update urls to use project identifier and issue sequence 2024-06-26 15:02:18 +05:30
pablohashescobar 6cd03cbb56 Merge branch 'preview' of github.com:makeplane/plane into chore-urls 2024-06-26 13:07:01 +05:30
a3a1e9cf9e [WEB-1684] chore: handled estimate dropdown render dynaically for in workspace and project level issues. (#4936)
* chore: handled estimate dropdown render dynaically for in workspace and project level issues.

* chore: issue manager updated

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-06-26 12:37:46 +05:30
pablohashescobar e6ecfcbbc7 dev: update urls to use project identifiers and issue sequence 2024-06-25 21:18:01 +05:30
sriram veeraghanta f087af3946 fix: export page root from component pages 2024-06-25 20:48:34 +05:30
Anmol Singh BhatiaandGitHub bafe3f9c45 fix: password strength banner validation (#4935) 2024-06-25 19:23:58 +05:30
99184371f7 [WEB-1727] refactor: pages editor sync logic solidified (#4926)
* feat: pages editor sync logic solidified

* chore: added validation for archive and lock in a page

* feat: pages editor sync logic solidified

* fix: updated the auto save hook to run every 10s instead of 10s after the user stops typing!!

* chore: custom status code for pages

* fix: forceSync in case of auto save

* fix: modifying a locked and archived page shows a toast for now!

* fix: build errors and better error messages

* chore: page root moved

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-06-25 18:58:57 +05:30
Prateek ShouryaandGitHub c919435598 style: admin app UI & UX updates. (#4934) 2024-06-25 18:22:25 +05:30
rahulrameshaandGitHub 635efeab7b [WEB-1255] chore: public and unlocked views (#4932)
* chore private and public views required changes

* fix slight alignment of view icons

* add feedback for update View button

* fix object reference sharing by using cloneDeep to replicate objects

* addressing review comments
2024-06-25 18:21:30 +05:30
Aaryan KhandelwalandGitHub 711494b72e [WEB-1716] fix: sidebar UI inconsistencies (#4933)
* fix: sidebar UI inconsistencies

* fix: minor UI fixes
2024-06-25 17:35:43 +05:30
guru_sainathandGitHub 245962d5a5 fix: resolved theme updation workflow in store-wrapper and workspace preferences (#4931) 2024-06-25 15:56:27 +05:30
guru_sainathandGitHub c5cd823aaa [WEB-522] fix: handling the issue and estimate point dropdown in the active cycle to render only when the estimate type is points (#4930)
* fix: handling the issue and estimate point dropdown in active cycle to render only when estimate type is points

* fix: reverted the theme store
2024-06-25 14:11:28 +05:30
Prateek ShouryaandGitHub 981a8e93ba [WEB-1691] fix: update ControlLink default target to _blank to avoid unwanted N-progress trigger. (#4927) 2024-06-25 12:32:51 +05:30
Anmol Singh BhatiaandGitHub 0ce17ff212 fix: module and cycle sidebar issue progress (#4922) 2024-06-25 12:31:42 +05:30
rahulrameshaandGitHub 7da5443808 fix getting issue filter params based on only spreadsheet for workspace views (#4923) 2024-06-25 12:27:44 +05:30
Aaryan KhandelwalandGitHub 535a27309e [WEB-1716] dev: app sidebar revamp (#4921)
* dev: revamp app sidebar

* dev: revamp app sidebar

* chore: tooltips added

* fix: drag handle

* chore: update chevron angle

* fix: workspace sidebar dropdown
2024-06-24 20:10:30 +05:30
Anmol Singh BhatiaandGitHub 3eda3845fa [WEB-1008] chore: issue toast alert improvement (#4914)
* chore: toast alert improvement

* chore: create issue toast action items component added

* chore: toast alert improvement

* chore: issue toast alert implementation

* fix: spreadsheet layout quick add toast action
2024-06-24 18:07:27 +05:30
Anmol Singh BhatiaandGitHub b6cf177630 chore: draft issue quick action menu validation (#4918) 2024-06-24 17:54:57 +05:30
guru_sainathandGitHub 04eca3c9aa [WEB-1499] fix: fixed onboarding multiple redirections (#4920)
* fix: fixed onboarding multiple redirections

* fix: removed else condition on redirection
2024-06-24 17:26:29 +05:30
Prateek ShouryaandGitHub e538bfad1f [WEB-1702] chore: fix project archives breadcrumbs and minor ui fixes in profile page. (#4916)
* [WEB-1702] chore: fix project archives breadcrumbs.

* chore: minor padding fix in profile page.

* chore: update archive page breadcrumbs display logic.

* chore: add/ update page title.
2024-06-24 17:15:35 +05:30
guru_sainathandGitHub 7c4c777c99 [WEB-1696] chore: rendering mention comments in notification modal (#4903)
* chore: rendering mention comments in notification modal

* chore: resolved build errors

* chore: updated workflow

* chore: updated function name and handled the notification mention render
2024-06-24 17:13:17 +05:30
guru_sainathandGitHub 08c4027e77 chore: updated redirection in archied estimates (#4919) 2024-06-24 17:06:42 +05:30
guru_sainathandGitHub a554d87531 fix: Input type number validation on estimate in create and update (#4917) 2024-06-24 13:49:31 +05:30
Aaryan KhandelwalandGitHub dcbd974ac5 fix: mentions not working in any editor (#4915) 2024-06-24 13:49:11 +05:30
guru_sainathandGitHub 1c8e709870 [WEB-522] chore: handled maximum no of char that user can enter in estimate point create and update (#4913)
* chore: handled maximum no of char that user can enter in estimate point create and update

* chore: typo
2024-06-24 13:27:59 +05:30
NikhilandGitHub 716a35779c chore: remove set password option in space app (#4912) 2024-06-24 12:54:20 +05:30
NikhilandGitHub ce9d541981 fix: pagination when group is not present in result (#4910) 2024-06-24 12:27:18 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3f7516fa9b chore(deps): bump ws from 8.17.0 to 8.17.1 (#4911)
Bumps [ws](https://github.com/websockets/ws) from 8.17.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.17.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 12:25:51 +05:30
sriram veeraghanta 2c33107ef4 fix: version number component creation 2024-06-23 12:58:30 +05:30
616e593bc2 [WEB-1685] chore: page and view list page improvement (#4906)
* chore: logo picker removed from page and view list page

* chore: admin sidebar improvement

* chore: minor improvement in app sidebar help section.

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2024-06-21 19:27:31 +05:30
Aaryan KhandelwalandGitHub 55a5c2d383 [WEB-1682] chore: additional document editor extensions (#4905)
* chore: additional document editor extensions

* chore: updated document editor hook
2024-06-21 18:53:35 +05:30
rahulrameshaandGitHub adec4e1f2d [WEB-1686] fix: issues incorrect filters while switching between, projects, modules etc (#4904)
* fix issues incorrect filters while switching between, projects, modules etc

* fix minor kanban pagination loader
2024-06-21 18:48:21 +05:30
dcdd1ef065 [WEB-1682] refactor: editor code splitting (#4893)
* refactor: lite and rich text editors

* refactor: document editor migration

* fix: add missing css import

* refactor: issue embed widget splitting

* chore: remove extensions folder from ee

* chore: update web ee folder structure

* fix: build errors

---------

Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
2024-06-21 17:37:11 +05:30
Aaryan KhandelwalandGitHub 367ccba17e [WEB-1690] fix: always show the search input if search query is present (#4901)
* fix: don't hide search input if search query is present

* chore: update logic

* chore: update dependency aray
2024-06-21 15:08:57 +05:30
guru_sainathandGitHub efab68b19d chore: updated workspace members in workspace settings members (#4902) 2024-06-21 14:38:36 +05:30
guru_sainathandGitHub 69d67fc02a chore: updated input validation on create and update estimate point form (#4900) 2024-06-21 13:27:56 +05:30
guru_sainathandGitHub 7bb1f7c210 [WEB-1681] fix: onboarding password validation on weak password (#4899)
* fix: onboarding password validation on weak passwrod

* fix: updated button disabled logic
2024-06-21 13:26:50 +05:30
Bavisetti NarayanandGitHub d164aa5117 chore: external api cycle snapshot (#4882)
* chore: external api cycle snapshot

* chore: removed estimates from list endpoint
2024-06-21 13:14:00 +05:30
Prateek ShouryaandGitHub 8d3755c433 [WEB-1670] style: fix tab highlights on user activity page. (#4898) 2024-06-20 20:07:23 +05:30
guru_sainathandGitHub c0ad5952df [WEB-522] chore: Move the estimates store and service out of core (#4896)
* chore: moved the estimate store from core to ce and ee

* chore: moved the estimate service from core to ce and ee

* chore: updated constructors from private to public in estimate store

* chore: exported estimate service
2024-06-20 19:29:09 +05:30
Prateek ShouryaandGitHub 94e6fd4b29 [WEB-1657] dev: admin application code structuring. (#4867)
* [WEB-1657] dev: admin application code structuring.

* chore: update mobx-react-lite import to mobx-react.
2024-06-20 17:52:01 +05:30
Anmol Singh BhatiaandGitHub 096d9b1541 fix: project label error message added and ui improvement (#4895) 2024-06-20 17:37:51 +05:30
guru_sainathandGitHub 1b1302dfbd chore: handled password validation on onboarding screen (#4894) 2024-06-20 17:27:28 +05:30
Anmol Singh BhatiaandGitHub 522cdc6873 style: emoji picker search section consistency (#4889) 2024-06-20 17:09:26 +05:30
Anmol Singh BhatiaandGitHub d638ed8882 choe: icon list sectoin improvement (#4890) 2024-06-20 17:08:49 +05:30
Anmol Singh BhatiaandGitHub 00de199a8d fix: issue peek view label with longer title (#4892) 2024-06-20 17:00:11 +05:30
guru_sainathandGitHub 01d785b9a9 [WEB-1681] chore: handled password strength validation and improved the acceptable char (#4891)
* chore: handled password validation on onboarding screen

* chore: updated is password focused
2024-06-20 16:58:26 +05:30
Prateek ShouryaandGitHub f06cce44a3 chore: enable posthog pageview. (#4888) 2024-06-20 16:52:05 +05:30
280a69bd3c [WEB-1669] chore: enable sign out and sentry on exception error page. (#4884)
* [WEB-1669] chore: enable sign out and sentry on exception error page.

* fix: global error handling

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-06-20 16:50:05 +05:30
c36c98476c chore: move FavoriteStar component to ui package (#4834)
* chore: move favorite star to ui package

* Update yarn.lock

---------

Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
2024-06-20 16:26:21 +05:30
Anmol Singh BhatiaandGitHub 24adddd29f [WEB-1647] chore: breadcrumb items loader (#4887)
* chore: breadcrumb item loader added

* chore: breadcrumb item loader implementation
2024-06-20 16:08:14 +05:30
Bavisetti NarayanandGitHub 4e5b445930 chore: workspace cycles (#4886) 2024-06-20 16:05:24 +05:30
Bavisetti NarayanandGitHub 198452430f [WEB-1674] chore: views access control (#4885)
* chore: access control changes in views

* chore: view serializer change
2024-06-20 16:04:46 +05:30
sriram veeraghanta f3bafb02d1 fix: change sync repo scope 2024-06-20 16:02:47 +05:30
sriram veeraghanta ba63e0c5ea fix: replacing mobx react lite with mobx react 2024-06-20 14:08:52 +05:30
f029683152 [WEB-1420] chore: considering telemetry settings while capturing events (#4582)
* chore: added telemetry configuration for cloud & selfhosted

* taking is_telemetry_enabled field only into consideration

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-06-20 13:58:42 +05:30
Prateek ShouryaandGitHub 268c35a584 fix: update root store import in space app. (#4883) 2024-06-20 13:32:42 +05:30
Bavisetti NarayanandGitHub 418ca92f36 [WEB-1665] chore: advance views migration (#4869)
* chore: views migration

* chore: squashed migration

* chore: renamed global views

* chore: added owned by in serializer
2024-06-20 13:12:33 +05:30
NikhilandGitHub aeb0979827 fix: pagination repeat on none filters (#4879) 2024-06-20 12:40:52 +05:30
Prateek ShouryaandGitHub 718453b332 [WEB-1610] chore: re-implement reload confirmation hook. (#4880)
* [WEB-1610] chore: re-implement reload confirmation hook.

* chore: attach anchor event listner to window instead of body.
2024-06-20 12:19:29 +05:30
rahulrameshaandGitHub e04eb1a63d fix issue creation logic for module and cycle issues (#4878) 2024-06-20 10:27:26 +05:30
guru_sainathandGitHub c6a3c0409f chore: handled estimation error when the input field is not empty (#4877) 2024-06-19 19:15:18 +05:30
Anmol Singh BhatiaandGitHub d65d89be36 chore: sign up form password strength improvement (#4876) 2024-06-19 18:37:17 +05:30
guru_sainathandGitHub 1bc80954c3 [WEB-1661] fix: on update project emoji window was closing (#4875)
* fix: on update project emoji window was closing

* fix: updated code structure
2024-06-19 18:18:25 +05:30
guru_sainathandGitHub 77baf4034b chore: Added swr to fetch the workspace members (#4874) 2024-06-19 18:15:01 +05:30
Prateek ShouryaandGitHub a2d691a446 [WEB-1652] fix: add optional channing to fix app crash if router params are not available. (#4871) 2024-06-19 16:54:50 +05:30
Prateek ShouryaandGitHub cff2161506 [WEB-1663] chore: imporve create new label UI. (#4873) 2024-06-19 16:53:46 +05:30
Aaryan KhandelwalandGitHub 46ac908155 chore: updated error and empty states' content (#4870) 2024-06-19 16:39:31 +05:30
guru_sainathandGitHub 67ad958998 [WEB-1616] chore: improving the graphs ui and label in analytics (#4872)
* chore: improving the graphs ui and label in analytics

* chore: automatic width in tabel
2024-06-19 16:38:44 +05:30
guru_sainathandGitHub 9b79a66a90 chore: updated Workspace views filter in all issues (#4868) 2024-06-19 13:43:42 +05:30
guru_sainathandGitHub 0b4faf7db2 [WEB-1642] chore: mutating issue estimation_point when we delete the estimate point from the project (#4862)
* chore: mutating issue estimation_point when we delete the eatimate point from the project

* chore: updated try catch

* chore: updated removed issues length check on estimate delete
2024-06-18 20:21:13 +05:30
guru_sainathandGitHub a06beaa312 chore: removed estimate select dropdown component from estimate points (#4863) 2024-06-18 20:04:25 +05:30
Prateek ShouryaandGitHub 8155d9a3ce [WEB-1624] chore: add un-snooze option and minor fixes in inbox issue (#4854)
* [WEB-1624] chore: add option to un-snooze inbox issue.

* [WEB-1605] fix: inbox issues snooze till date consistency.

* chore: update snooze/ un-snooze logic for issues for which snoozed till date is passed.
2024-06-18 19:59:28 +05:30
720 changed files with 6449 additions and 6234 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
push:
branches:
- develop
- preview
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: "AI Settings - God Mode",
title: "Artificial Intelligence Settings - Plane Web",
};
export default function AILayout({ children }: { children: ReactNode }) {
+1 -4
View File
@@ -1,10 +1,8 @@
"use client";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// components
@@ -18,7 +16,6 @@ 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>
+4 -4
View File
@@ -1,13 +1,14 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// hooks
@@ -16,7 +17,6 @@ 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="Authentication - God Mode" />
<PageHeader title="GitHub Authentication - Plane Web" />
<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-lite";
import { observer } from "mobx-react";
import Image from "next/image";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
// 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="Authentication - God Mode" />
<PageHeader title="GitLab Authentication - Plane Web" />
<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-lite";
import { observer } from "mobx-react";
import Image from "next/image";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
// 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="Authentication - God Mode" />
<PageHeader title="Google Authentication - Plane Web" />
<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 - God Mode",
title: "Authentication Settings - Plane Web",
};
export default function AuthenticationLayout({ children }: { children: ReactNode }) {
+10 -96
View File
@@ -1,41 +1,16 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useTheme } from "next-themes";
import { observer } from "mobx-react";
import useSWR from "swr";
import { Mails, KeyRound } from "lucide-react";
import { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
// hooks
// helpers
import { cn, resolveGeneralTheme } from "@/helpers/common.helper";
import { cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
// 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;
};
// plane admin components
import { AuthenticationModes } from "@/plane-admin/components/authentication";
const InstanceAuthenticationPage = observer(() => {
// store
@@ -45,8 +20,6 @@ const InstanceAuthenticationPage = observer(() => {
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// theme
const { resolvedTheme } = useTheme();
// derived values
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
@@ -60,7 +33,7 @@ const InstanceAuthenticationPage = observer(() => {
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...",
loading: "Saving configuration",
success: {
title: "Success",
message: () => "Configuration saved successfully",
@@ -81,58 +54,11 @@ 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 for your instance</div>
<div className="text-xl font-medium text-custom-text-100">Manage authentication modes 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>
@@ -140,15 +66,12 @@ 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={cn("font-medium leading-5 text-custom-text-100 text-sm")}>
Allow anyone to sign up without invite
</div>
<div className="text-lg font-medium pb-1">Allow anyone to sign up even without an invite</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
Toggling this off will disable self sign ups.
Toggling this off will only let users sign up when they are invited.
</div>
</div>
</div>
@@ -168,16 +91,7 @@ const InstanceAuthenticationPage = observer(() => {
</div>
</div>
<div className="text-lg font-medium pt-6">Authentication modes</div>
{authenticationMethodsCard.map((method) => (
<AuthenticationMethodCard
key={method.key}
name={method.name}
description={method.description}
icon={method.icon}
config={method.config}
disabled={isSubmitting}
/>
))}
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
</div>
) : (
<Loader className="space-y-10">
+1 -1
View File
@@ -7,7 +7,7 @@ interface EmailLayoutProps {
}
export const metadata: Metadata = {
title: "Email Settings - God Mode",
title: "Email Settings - Plane Web",
};
export default function EmailLayout({ children }: EmailLayoutProps) {
+1 -4
View File
@@ -1,10 +1,8 @@
"use client";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// components
@@ -18,7 +16,6 @@ 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-lite";
import { observer } from "mobx-react";
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 - God Mode",
title: "General Settings - Plane Web",
};
export default function GeneralLayout({ children }: { children: ReactNode }) {
+1 -1
View File
@@ -1,5 +1,5 @@
"use client";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
// hooks
import { useInstance } from "@/hooks/store";
// components
+1 -1
View File
@@ -7,7 +7,7 @@ interface ImageLayoutProps {
}
export const metadata: Metadata = {
title: "Images Settings - God Mode",
title: "Images Settings - Plane Web",
};
export default function ImageLayout({ children }: ImageLayoutProps) {
+1 -4
View File
@@ -1,10 +1,8 @@
"use client";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// local
@@ -18,7 +16,6 @@ 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>
+5 -3
View File
@@ -16,10 +16,12 @@ import { UserProvider } from "@/lib/user-provider";
// styles
import "./globals.css";
export default function RootLayout({ children }: { children: ReactNode }) {
// themes
const ToastWithTheme = () => {
const { resolvedTheme } = useTheme();
return <Toast theme={resolveGeneralTheme(resolvedTheme)} />;
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
@@ -31,7 +33,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
</head>
<body className={`antialiased`}>
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
<ToastWithTheme />
<SWRConfig value={SWR_CONFIG}>
<StoreProvider>
<InstanceProvider>
@@ -0,0 +1,104 @@
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}
/>
))}
</>
);
});
@@ -0,0 +1 @@
export * from "./authentication-modes";
@@ -1,69 +0,0 @@
"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
@@ -1,13 +0,0 @@
"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
@@ -1 +0,0 @@
export * from "./page-header";
@@ -1,7 +1,7 @@
"use client";
import { FC, useState, useRef } from "react";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
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-lite";
import { observer } from "mobx-react";
// 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-[250px]" : ""}
sm:${isSidebarCollapsed ? "-ml-[250px]" : ""}
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[250px]"}
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[250px]"}
${isSidebarCollapsed ? "-ml-[280px]" : ""}
sm:${isSidebarCollapsed ? "-ml-[280px]" : ""}
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[280px]"}
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[280px]"}
`}
>
<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-lite";
import { observer } from "mobx-react";
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-lite";
import { observer } from "mobx-react";
// hooks
import { Menu } from "lucide-react";
import { useTheme } from "@/hooks/store";
@@ -1,6 +1,6 @@
"use client";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
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-lite";
import { observer } from "mobx-react";
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();
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
// 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-lite";
import { observer } from "mobx-react";
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-lite";
import { observer } from "mobx-react";
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-lite";
import { observer } from "mobx-react";
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-lite";
import { observer } from "mobx-react";
// hooks
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
@@ -6,4 +6,4 @@ export * from "./password-strength-meter";
export * from "./banner";
export * from "./empty-state";
export * from "./logo-spinner";
export * from "./toast";
export * from "./page-header";
@@ -0,0 +1,94 @@
"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>
);
};
@@ -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 { getPasswordStrength } from "@/helpers/password.helper";
import { E_PASSWORD_STRENGTH, 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) >= 3 &&
getPasswordStrength(formData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
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>
)}
{isPasswordInputFocused && <PasswordStrengthMeter password={formData.password} />}
<PasswordStrengthMeter password={formData.password} isFocused={isPasswordInputFocused} />
</div>
<div className="w-full space-y-1">
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme as nextUseTheme } from "next-themes";
// ui
@@ -1,6 +1,6 @@
"use client";
import { FC, ReactNode, useEffect } from "react";
import { observer } from "mobx-react-lite";
import { observer } from "mobx-react";
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-lite";
import { observer } from "mobx-react";
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-lite";
import { observer } from "mobx-react";
import useSWR from "swr";
// hooks
import { useInstance, useTheme, useUser } from "@/hooks/store";
@@ -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";
import { EInstanceStatus, TInstanceStatus } from "@/helpers/instance.helper";
// services
import { InstanceService } from "@/services/instance.service";
// root store
@@ -1,4 +1,4 @@
import { enableStaticRendering } from "mobx-react-lite";
import { enableStaticRendering } from "mobx-react";
// 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";
import { EUserStatus, TUserStatus } from "@/helpers/user.helper";
// services
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
@@ -0,0 +1 @@
export * from "ce/components/authentication/authentication-modes";
@@ -0,0 +1 @@
export * from "./authentication-modes";
+61 -10
View File
@@ -1,16 +1,67 @@
import zxcvbn from "zxcvbn";
export const isPasswordCriteriaMet = (password: string) => {
const criteria = [password.length >= 8, /[A-Z]/.test(password), /\d/.test(password), /[!@#$%^&*]/.test(password)];
export enum E_PASSWORD_STRENGTH {
EMPTY = "empty",
LENGTH_NOT_VALID = "length_not_valid",
STRENGTH_NOT_VALID = "strength_not_valid",
STRENGTH_VALID = "strength_valid",
}
return criteria.every((criterion) => criterion);
};
const PASSWORD_MIN_LENGTH = 8;
// const PASSWORD_NUMBER_REGEX = /\d/;
// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/;
// const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/;
export const getPasswordStrength = (password: string) => {
if (password.length === 0) return 0;
if (password.length < 8) return 1;
if (!isPasswordCriteriaMet(password)) return 2;
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),
// },
];
const result = zxcvbn(password);
return result.score;
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;
};
+1 -1
View File
@@ -22,7 +22,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.356.0",
"mobx": "^6.12.0",
"mobx-react-lite": "^4.0.5",
"mobx-react": "^9.1.1",
"next": "^14.2.3",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
+4 -1
View File
@@ -7,7 +7,10 @@
"jsx": "preserve",
"esModuleInterop": true,
"paths": {
"@/*": ["*"]
"@/*": ["core/*"],
"@/helpers/*": ["helpers/*"],
"@/public/*": ["public/*"],
"@/plane-admin/*": ["ce/*"]
},
"plugins": [
{
+177 -56
View File
@@ -3,9 +3,18 @@ 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
@@ -22,6 +31,7 @@ from plane.db.models import (
Cycle,
CycleIssue,
Issue,
Project,
IssueAttachment,
IssueLink,
)
@@ -115,29 +125,6 @@ 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()
)
@@ -784,7 +771,6 @@ 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(
@@ -792,9 +778,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
new_cycle = Cycle.objects.get(
new_cycle = Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
)
).first()
old_cycle = (
Cycle.objects.filter(
@@ -861,18 +847,131 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
)
# 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,
)
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
]
# Get the assignee distribution
assignee_distribution = (
Issue.objects.filter(
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
@@ -884,7 +983,10 @@ 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(
@@ -926,7 +1028,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
# Get the label distribution
label_distribution = (
Issue.objects.filter(
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
@@ -938,8 +1040,11 @@ 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(
@@ -979,26 +1084,42 @@ 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()
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"])
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 (
new_cycle.end_date is not None
@@ -37,7 +37,6 @@ from .project import (
)
from .state import StateSerializer, StateLiteSerializer
from .view import (
GlobalViewSerializer,
IssueViewSerializer,
)
from .cycle import (
+33 -20
View File
@@ -44,12 +44,16 @@ 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
@@ -61,29 +65,38 @@ class ProjectSerializer(BaseSerializer):
project = super().update(instance, validated_data)
return project
# 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
# When updating check if the project identifier is different
if instance.identifier == identifier:
project = super().update(instance, validated_data)
return project
# If not same fail update
raise serializers.ValidationError(
detail="Project Identifier is already taken"
# 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,
)
# Return the updated project
return project
class ProjectLiteSerializer(BaseSerializer):
+5 -39
View File
@@ -2,50 +2,13 @@
from rest_framework import serializers
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import GlobalView, IssueView
from .base import DynamicBaseSerializer
from plane.db.models import 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
@@ -54,6 +17,9 @@ 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/<uuid:project_id>/cycles/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/",
CycleViewSet.as_view(
{
"get": "list",
@@ -24,7 +24,7 @@ urlpatterns = [
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:pk>/",
CycleViewSet.as_view(
{
"get": "retrieve",
@@ -36,7 +36,7 @@ urlpatterns = [
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/cycles/date-check/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/date-check/",
CycleDateCheckEndpoint.as_view(),
name="project-cycle-date",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view(
{
"get": "list",
@@ -73,7 +73,7 @@ urlpatterns = [
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
TransferCycleIssueEndpoint.as_view(),
name="transfer-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/user-properties/",
CycleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/archive/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/archive/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/",
"workspaces/<str:slug>/projects/<str:project_id>/archived-cycles/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/project-estimates/",
"workspaces/<str:slug>/projects/<str:project_id>/project-estimates/",
ProjectEstimatePointEndpoint.as_view(),
name="project-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
"workspaces/<str:slug>/projects/<str:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
@@ -25,7 +25,7 @@ urlpatterns = [
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
EstimatePointEndpoint.as_view(
{
"patch": "partial_update",
+6 -3
View File
@@ -2,7 +2,10 @@ 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 = [
@@ -12,11 +15,11 @@ urlpatterns = [
name="unsplash",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/inboxes/",
"workspaces/<str:slug>/projects/<str:project_id>/inboxes/",
InboxViewSet.as_view(
{
"get": "list",
@@ -19,7 +19,7 @@ urlpatterns = [
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/inboxes/<uuid:pk>/",
InboxViewSet.as_view(
{
"get": "retrieve",
@@ -30,7 +30,7 @@ urlpatterns = [
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/inbox-issues/",
InboxIssueViewSet.as_view(
{
"get": "list",
@@ -40,7 +40,7 @@ urlpatterns = [
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/issues/list/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/list/",
IssueListEndpoint.as_view(),
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/",
IssueViewSet.as_view(
{
"get": "list",
@@ -40,7 +40,7 @@ urlpatterns = [
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:pk>/",
IssueViewSet.as_view(
{
"get": "retrieve",
@@ -52,7 +52,7 @@ urlpatterns = [
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
"workspaces/<str:slug>/projects/<str:project_id>/issue-labels/",
LabelViewSet.as_view(
{
"get": "list",
@@ -62,7 +62,7 @@ urlpatterns = [
name="project-issue-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/bulk-create-labels/",
"workspaces/<str:slug>/projects/<str:project_id>/bulk-create-labels/",
BulkCreateIssueLabelsEndpoint.as_view(),
name="project-bulk-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-delete-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/bulk-delete-issues/",
BulkDeleteIssuesEndpoint.as_view(),
name="project-issues-bulk",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-archive-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/bulk-archive-issues/",
BulkArchiveIssuesEndpoint.as_view(),
name="bulk-archive-issues",
),
##
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/sub-issues/",
SubIssuesEndpoint.as_view(),
name="sub-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-links/",
IssueLinkViewSet.as_view(
{
"get": "list",
@@ -105,7 +105,7 @@ urlpatterns = [
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str: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/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-attachments/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str: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/<uuid:project_id>/issues/<uuid:issue_id>/history/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/history/",
IssueActivityEndpoint.as_view(),
name="project-issue-history",
),
## Issue Activity
## IssueComments
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/comments/",
IssueCommentViewSet.as_view(
{
"get": "list",
@@ -151,7 +151,7 @@ urlpatterns = [
name="project-issue-comment",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/comments/<uuid:pk>/",
IssueCommentViewSet.as_view(
{
"get": "retrieve",
@@ -165,7 +165,7 @@ urlpatterns = [
## End IssueComments
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view(
{
"get": "list",
@@ -175,12 +175,12 @@ urlpatterns = [
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
IssueSubscriberViewSet.as_view({"delete": "destroy"}),
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{
"get": "subscription_status",
@@ -193,7 +193,7 @@ urlpatterns = [
## End Issue Subscribers
# Issue Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/reactions/",
IssueReactionViewSet.as_view(
{
"get": "list",
@@ -203,7 +203,7 @@ urlpatterns = [
name="project-issue-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str: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/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/user-properties/",
"workspaces/<str:slug>/projects/<str:project_id>/user-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
## IssueProperty End
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/archived-issues/",
IssueArchiveViewSet.as_view(
{
"get": "list",
@@ -251,7 +251,7 @@ urlpatterns = [
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"get": "list",
@@ -274,7 +274,7 @@ urlpatterns = [
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str: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/<uuid:project_id>/issue-drafts/",
"workspaces/<str:slug>/projects/<str:project_id>/issue-drafts/",
IssueDraftViewSet.as_view(
{
"get": "list",
@@ -295,7 +295,7 @@ urlpatterns = [
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/bulk-operation-issues/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/modules/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/",
ModuleViewSet.as_view(
{
"get": "list",
@@ -23,7 +23,7 @@ urlpatterns = [
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:pk>/",
ModuleViewSet.as_view(
{
"get": "retrieve",
@@ -35,7 +35,7 @@ urlpatterns = [
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/modules/<uuid:module_id>/issues/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/modules/<uuid:module_id>/module-links/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/user-favorite-modules/",
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-modules/",
ModuleFavoriteViewSet.as_view(
{
"get": "list",
@@ -98,7 +98,7 @@ urlpatterns = [
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/user-properties/",
ModuleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/archive/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/archive/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/",
"workspaces/<str:slug>/projects/<str:project_id>/archived-modules/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/pages/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
@@ -22,7 +22,7 @@ urlpatterns = [
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
@@ -34,7 +34,7 @@ urlpatterns = [
),
# favorite pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/favorite-pages/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/favorite-pages/<uuid:pk>/",
PageFavoriteViewSet.as_view(
{
"post": "create",
@@ -45,7 +45,7 @@ urlpatterns = [
),
# archived pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/archive/",
PageViewSet.as_view(
{
"post": "archive",
@@ -56,7 +56,7 @@ urlpatterns = [
),
# lock and unlock
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/lock/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/pages/<uuid:pk>/transactions/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/transactions/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/<uuid:transaction>/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/sub-pages/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/sub-pages/",
SubPagesEndpoint.as_view(),
name="sub-page",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
"workspaces/<str:slug>/projects/<str: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/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:pk>/",
ProjectViewSet.as_view(
{
"get": "retrieve",
@@ -47,7 +47,7 @@ urlpatterns = [
name="project-identifiers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
"workspaces/<str:slug>/projects/<str:project_id>/invitations/",
ProjectInvitationsViewset.as_view(
{
"get": "list",
@@ -57,7 +57,7 @@ urlpatterns = [
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/invitations/<uuid:pk>/",
ProjectInvitationsViewset.as_view(
{
"get": "retrieve",
@@ -82,12 +82,12 @@ urlpatterns = [
name="user-project-roles",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/join/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/join/<uuid:pk>/",
ProjectJoinEndpoint.as_view(),
name="project-join",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
"workspaces/<str:slug>/projects/<str:project_id>/members/",
ProjectMemberViewSet.as_view(
{
"get": "list",
@@ -97,7 +97,7 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/members/<uuid:pk>/",
ProjectMemberViewSet.as_view(
{
"get": "retrieve",
@@ -108,7 +108,7 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
"workspaces/<str:slug>/projects/<str:project_id>/members/leave/",
ProjectMemberViewSet.as_view(
{
"post": "leave",
@@ -117,17 +117,17 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
"workspaces/<str:slug>/projects/<str:project_id>/team-invite/",
AddTeamToProjectEndpoint.as_view(),
name="projects",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
"workspaces/<str:slug>/projects/<str:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(),
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/me/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/",
"workspaces/<str:slug>/user-favorite-projects/<str:project_id>/",
ProjectFavoritesViewSet.as_view(
{
"delete": "destroy",
@@ -156,7 +156,7 @@ urlpatterns = [
name="project-covers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
"workspaces/<str:slug>/projects/<str:project_id>/project-deploy-boards/",
DeployBoardViewSet.as_view(
{
"get": "list",
@@ -166,7 +166,7 @@ urlpatterns = [
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/archive/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/search-issues/",
"workspaces/<str:slug>/projects/<str: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/<uuid:project_id>/states/",
"workspaces/<str:slug>/projects/<str:project_id>/states/",
StateViewSet.as_view(
{
"get": "list",
@@ -16,7 +16,7 @@ urlpatterns = [
name="project-states",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/states/<uuid:pk>/",
StateViewSet.as_view(
{
"get": "retrieve",
@@ -27,7 +27,7 @@ urlpatterns = [
name="project-state",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/mark-default/",
"workspaces/<str:slug>/projects/<str: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,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
WorkspaceViewViewSet,
WorkspaceViewIssuesViewSet,
IssueViewFavoriteViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
"workspaces/<str:slug>/projects/<str:project_id>/views/",
IssueViewViewSet.as_view(
{
"get": "list",
@@ -21,7 +21,7 @@ urlpatterns = [
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/views/<uuid:pk>/",
IssueViewViewSet.as_view(
{
"get": "retrieve",
@@ -34,7 +34,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/views/",
GlobalViewViewSet.as_view(
WorkspaceViewViewSet.as_view(
{
"get": "list",
"post": "create",
@@ -44,7 +44,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/views/<uuid:pk>/",
GlobalViewViewSet.as_view(
WorkspaceViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
@@ -56,7 +56,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/issues/",
GlobalViewIssuesViewSet.as_view(
WorkspaceViewIssuesViewSet.as_view(
{
"get": "list",
}
@@ -64,7 +64,7 @@ urlpatterns = [
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
@@ -74,7 +74,7 @@ urlpatterns = [
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
"workspaces/<str:slug>/projects/<str: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 (
GlobalViewViewSet,
GlobalViewIssuesViewSet,
WorkspaceViewViewSet,
WorkspaceViewIssuesViewSet,
IssueViewViewSet,
IssueViewFavoriteViewSet,
)
+59 -4
View File
@@ -1,12 +1,12 @@
# Python imports
import traceback
import uuid
import zoneinfo
# Django imports
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,6 +24,8 @@ 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:
@@ -60,6 +62,44 @@ 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()
@@ -116,8 +156,22 @@ 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
@@ -125,6 +179,7 @@ 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)
@@ -138,7 +193,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
def project_id(self):
project_id = self.kwargs.get("project_id", None)
if project_id:
return project_id
return self.get_project_id(project_id)
if resolve(self.request.path_info).url_name == "project":
return self.kwargs.get("pk", None)
+3 -4
View File
@@ -177,7 +177,6 @@ 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()
@@ -279,7 +278,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
# Assignee Distribution
assignee_distribution = (
Issue.objects.filter(
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
workspace__slug=slug,
project_id=project_id,
@@ -327,7 +326,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
# Label Distribution
label_distribution = (
Issue.objects.filter(
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
workspace__slug=slug,
project_id=project_id,
@@ -376,7 +375,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
queryset=queryset,
slug=slug,
project_id=project_id,
plot_type=plot_type,
plot_type="issues",
cycle_id=pk,
)
+4 -4
View File
@@ -1223,7 +1223,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
if estimate_type:
assignee_estimate_data = (
Issue.objects.filter(
Issue.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.objects.filter(
Issue.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.objects.filter(
Issue.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.objects.filter(
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
+84 -54
View File
@@ -44,6 +44,7 @@ from plane.db.models import (
IssueReaction,
IssueSubscriber,
Project,
IssueSequence,
)
from plane.utils.grouper import (
issue_group_values,
@@ -58,8 +59,7 @@ from plane.utils.paginator import (
)
from .. import BaseAPIView, BaseViewSet
from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from plane.utils.valid_int_uuid import is_int_or_uuid
class IssueListEndpoint(BaseAPIView):
@@ -197,6 +197,23 @@ 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(
@@ -410,67 +427,76 @@ 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()
.filter(pk=pk)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
(
self.get_queryset()
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=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),
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())),
),
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"
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_attachment",
queryset=IssueAttachment.objects.select_related("issue"),
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
)
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related(
"issue"
),
)
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
.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,
)
)
)
)
).first()
.filter(pk=pk)
.first()
)
if not issue:
return Response(
{"error": "The required object does not exist."},
@@ -481,6 +507,7 @@ 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:
@@ -515,8 +542,11 @@ 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.objects.filter(
Issue.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.objects.filter(
Issue.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.objects.filter(
Issue.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.objects.filter(
Issue.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.objects.filter(
Issue.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.objects.filter(
Issue.issue_objects.filter(
issue_module__module_id=pk,
workspace__slug=slug,
project_id=project_id,
+24 -4
View File
@@ -431,8 +431,12 @@ class PagesDescriptionViewSet(BaseViewSet):
]
def retrieve(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=pk, workspace__slug=slug, projects__id=project_id
page = (
Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id
)
.filter(Q(owned_by=self.request.user) | Q(access=0))
.first()
)
binary_data = page.description_binary
@@ -451,10 +455,26 @@ class PagesDescriptionViewSet(BaseViewSet):
return response
def partial_update(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=pk, workspace__slug=slug, projects__id=project_id
page = (
Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id
)
.filter(Q(owned_by=self.request.user) | Q(access=0))
.first()
)
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