Compare commits

...

71 Commits

Author SHA1 Message Date
NarayanBavisetti d3f1841866 chore: code cleanup 2024-06-26 13:36:13 +05:30
guru_sainath 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
sriram veeraghanta f087af3946 fix: export page root from component pages 2024-06-25 20:48:34 +05:30
Anmol Singh Bhatia bafe3f9c45 fix: password strength banner validation (#4935) 2024-06-25 19:23:58 +05:30
M. Palanikannan 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 Shourya c919435598 style: admin app UI & UX updates. (#4934) 2024-06-25 18:22:25 +05:30
rahulramesha 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 Khandelwal 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_sainath 245962d5a5 fix: resolved theme updation workflow in store-wrapper and workspace preferences (#4931) 2024-06-25 15:56:27 +05:30
guru_sainath 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 Shourya 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 Bhatia 0ce17ff212 fix: module and cycle sidebar issue progress (#4922) 2024-06-25 12:31:42 +05:30
rahulramesha 7da5443808 fix getting issue filter params based on only spreadsheet for workspace views (#4923) 2024-06-25 12:27:44 +05:30
Aaryan Khandelwal 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 Bhatia 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 Bhatia b6cf177630 chore: draft issue quick action menu validation (#4918) 2024-06-24 17:54:57 +05:30
guru_sainath 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 Shourya 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_sainath 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_sainath 08c4027e77 chore: updated redirection in archied estimates (#4919) 2024-06-24 17:06:42 +05:30
guru_sainath a554d87531 fix: Input type number validation on estimate in create and update (#4917) 2024-06-24 13:49:31 +05:30
Aaryan Khandelwal dcbd974ac5 fix: mentions not working in any editor (#4915) 2024-06-24 13:49:11 +05:30
guru_sainath 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
Nikhil 716a35779c chore: remove set password option in space app (#4912) 2024-06-24 12:54:20 +05:30
Nikhil ce9d541981 fix: pagination when group is not present in result (#4910) 2024-06-24 12:27:18 +05:30
dependabot[bot] 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
Anmol Singh Bhatia 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 Khandelwal 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
rahulramesha 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
Aaryan Khandelwal 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 Khandelwal 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_sainath efab68b19d chore: updated workspace members in workspace settings members (#4902) 2024-06-21 14:38:36 +05:30
guru_sainath 69d67fc02a chore: updated input validation on create and update estimate point form (#4900) 2024-06-21 13:27:56 +05:30
guru_sainath 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 Narayan 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 Shourya 8d3755c433 [WEB-1670] style: fix tab highlights on user activity page. (#4898) 2024-06-20 20:07:23 +05:30
guru_sainath 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 Shourya 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 Bhatia 096d9b1541 fix: project label error message added and ui improvement (#4895) 2024-06-20 17:37:51 +05:30
guru_sainath 1b1302dfbd chore: handled password validation on onboarding screen (#4894) 2024-06-20 17:27:28 +05:30
Anmol Singh Bhatia 522cdc6873 style: emoji picker search section consistency (#4889) 2024-06-20 17:09:26 +05:30
Anmol Singh Bhatia d638ed8882 choe: icon list sectoin improvement (#4890) 2024-06-20 17:08:49 +05:30
Anmol Singh Bhatia 00de199a8d fix: issue peek view label with longer title (#4892) 2024-06-20 17:00:11 +05:30
guru_sainath 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 Shourya f06cce44a3 chore: enable posthog pageview. (#4888) 2024-06-20 16:52:05 +05:30
Prateek Shourya 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
Aaryan Khandelwal 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 Bhatia 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 Narayan 4e5b445930 chore: workspace cycles (#4886) 2024-06-20 16:05:24 +05:30
Bavisetti Narayan 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
Lakhan Baheti 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 Shourya 268c35a584 fix: update root store import in space app. (#4883) 2024-06-20 13:32:42 +05:30
Bavisetti Narayan 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
Nikhil aeb0979827 fix: pagination repeat on none filters (#4879) 2024-06-20 12:40:52 +05:30
Prateek Shourya 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
rahulramesha e04eb1a63d fix issue creation logic for module and cycle issues (#4878) 2024-06-20 10:27:26 +05:30
guru_sainath c6a3c0409f chore: handled estimation error when the input field is not empty (#4877) 2024-06-19 19:15:18 +05:30
Anmol Singh Bhatia d65d89be36 chore: sign up form password strength improvement (#4876) 2024-06-19 18:37:17 +05:30
guru_sainath 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_sainath 77baf4034b chore: Added swr to fetch the workspace members (#4874) 2024-06-19 18:15:01 +05:30
Prateek Shourya 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 Shourya cff2161506 [WEB-1663] chore: imporve create new label UI. (#4873) 2024-06-19 16:53:46 +05:30
Aaryan Khandelwal 46ac908155 chore: updated error and empty states' content (#4870) 2024-06-19 16:39:31 +05:30
guru_sainath 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_sainath 9b79a66a90 chore: updated Workspace views filter in all issues (#4868) 2024-06-19 13:43:42 +05:30
guru_sainath 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_sainath a06beaa312 chore: removed estimate select dropdown component from estimate points (#4863) 2024-06-18 20:04:25 +05:30
Prateek Shourya 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
704 changed files with 6024 additions and 6280 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";
+63 -12
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);
};
export const getPasswordStrength = (password: string) => {
if (password.length === 0) return 0;
if (password.length < 8) return 1;
if (!isPasswordCriteriaMet(password)) return 2;
const result = zxcvbn(password);
return result.score;
const PASSWORD_MIN_LENGTH = 8;
// const PASSWORD_NUMBER_REGEX = /\d/;
// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/;
// const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/;
export const PASSWORD_CRITERIA = [
{
key: "min_8_char",
label: "Min 8 characters",
isCriteriaValid: (password: string) => password.length >= PASSWORD_MIN_LENGTH,
},
// {
// key: "min_1_upper_case",
// label: "Min 1 upper-case letter",
// isCriteriaValid: (password: string) => PASSWORD_NUMBER_REGEX.test(password),
// },
// {
// key: "min_1_number",
// label: "Min 1 number",
// isCriteriaValid: (password: string) => PASSWORD_CHAR_CAPS_REGEX.test(password),
// },
// {
// key: "min_1_special_char",
// label: "Min 1 special character",
// isCriteriaValid: (password: string) => PASSWORD_SPECIAL_CHAR_REGEX.test(password),
// },
];
export const getPasswordStrength = (password: string): 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 (
+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):
-6
View File
@@ -19,7 +19,6 @@ from plane.app.views import (
IssueUserDisplayPropertyEndpoint,
IssueViewSet,
LabelViewSet,
BulkIssueOperationsEndpoint,
BulkArchiveIssuesEndpoint,
)
@@ -305,9 +304,4 @@ urlpatterns = [
),
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-operation-issues/",
BulkIssueOperationsEndpoint.as_view(),
name="bulk-operations-issues",
),
]
+5 -5
View File
@@ -3,8 +3,8 @@ from django.urls import path
from plane.app.views import (
IssueViewViewSet,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
WorkspaceViewViewSet,
WorkspaceViewIssuesViewSet,
IssueViewFavoriteViewSet,
)
@@ -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",
}
+2 -5
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,
)
@@ -151,9 +151,6 @@ from .issue.subscriber import (
IssueSubscriberViewSet,
)
from .issue.bulk_operations import BulkIssueOperationsEndpoint
from .module.base import (
ModuleViewSet,
ModuleLinkViewSet,
+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,
@@ -1,288 +0,0 @@
# Python imports
import json
from datetime import datetime
# Django imports
from django.utils import timezone
# Third Party imports
from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseAPIView
from plane.app.permissions import (
ProjectEntityPermission,
)
from plane.db.models import (
Project,
Issue,
IssueLabel,
IssueAssignee,
)
from plane.bgtasks.issue_activites_task import issue_activity
class BulkIssueOperationsEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id):
issue_ids = request.data.get("issue_ids", [])
if not len(issue_ids):
return Response(
{"error": "Issue IDs are required"},
status=status.HTTP_400_BAD_REQUEST,
)
# Get all the issues
issues = (
Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
)
.select_related("state")
.prefetch_related("labels", "assignees")
)
# Current epoch
epoch = int(timezone.now().timestamp())
# Project details
project = Project.objects.get(workspace__slug=slug, pk=project_id)
workspace_id = project.workspace_id
# Initialize arrays
bulk_update_issues = []
bulk_issue_activities = []
bulk_update_issue_labels = []
bulk_update_issue_assignees = []
properties = request.data.get("properties", {})
if properties.get("start_date", False) and properties.get("target_date", False):
if (
datetime.strptime(properties.get("start_date"), "%Y-%m-%d").date()
> datetime.strptime(properties.get("target_date"), "%Y-%m-%d").date()
):
return Response(
{
"error_code": 4100,
"error_message": "INVALID_ISSUE_DATES",
},
status=status.HTTP_400_BAD_REQUEST,
)
for issue in issues:
# Priority
if properties.get("priority", False):
bulk_issue_activities.append(
{
"type": "issue.activity.updated",
"requested_data": json.dumps(
{"priority": properties.get("priority")}
),
"current_instance": json.dumps(
{"priority": (issue.priority)}
),
"issue_id": str(issue.id),
"actor_id": str(request.user.id),
"project_id": str(project_id),
"epoch": epoch,
}
)
issue.priority = properties.get("priority")
# State
if properties.get("state_id", False):
bulk_issue_activities.append(
{
"type": "issue.activity.updated",
"requested_data": json.dumps(
{"state": properties.get("state")}
),
"current_instance": json.dumps(
{"state": str(issue.state_id)}
),
"issue_id": str(issue.id),
"actor_id": str(request.user.id),
"project_id": str(project_id),
"epoch": epoch,
}
)
issue.state_id = properties.get("state_id")
# Start date
if properties.get("start_date", False):
if (
issue.target_date
and not properties.get("target_date", False)
and issue.target_date
<= datetime.strptime(
properties.get("start_date"), "%Y-%m-%d"
).date()
):
return Response(
{
"error_code": 4101,
"error_message": "INVALID_ISSUE_START_DATE",
},
status=status.HTTP_400_BAD_REQUEST,
)
bulk_issue_activities.append(
{
"type": "issue.activity.updated",
"requested_data": json.dumps(
{"start_date": properties.get("start_date")}
),
"current_instance": json.dumps(
{"start_date": str(issue.start_date)}
),
"issue_id": str(issue.id),
"actor_id": str(request.user.id),
"project_id": str(project_id),
"epoch": epoch,
}
)
issue.start_date = properties.get("start_date")
# Target date
if properties.get("target_date", False):
if (
issue.start_date
and not properties.get("start_date", False)
and issue.start_date
>= datetime.strptime(
properties.get("target_date"), "%Y-%m-%d"
).date()
):
return Response(
{
"error_code": 4102,
"error_message": "INVALID_ISSUE_TARGET_DATE",
},
status=status.HTTP_400_BAD_REQUEST,
)
bulk_issue_activities.append(
{
"type": "issue.activity.updated",
"requested_data": json.dumps(
{"target_date": properties.get("target_date")}
),
"current_instance": json.dumps(
{"target_date": str(issue.target_date)}
),
"issue_id": str(issue.id),
"actor_id": str(request.user.id),
"project_id": str(project_id),
"epoch": epoch,
}
)
issue.target_date = properties.get("target_date")
bulk_update_issues.append(issue)
# Labels
if properties.get("label_ids", []):
for label_id in properties.get("label_ids", []):
bulk_update_issue_labels.append(
IssueLabel(
issue=issue,
label_id=label_id,
created_by=request.user,
project_id=project_id,
workspace_id=workspace_id,
)
)
bulk_issue_activities.append(
{
"type": "issue.activity.updated",
"requested_data": json.dumps(
{"label_ids": properties.get("label_ids", [])}
),
"current_instance": json.dumps(
{
"label_ids": [
str(label.id)
for label in issue.labels.all()
]
}
),
"issue_id": str(issue.id),
"actor_id": str(request.user.id),
"project_id": str(project_id),
"epoch": epoch,
}
)
# Assignees
if properties.get("assignee_ids", []):
for assignee_id in properties.get(
"assignee_ids", issue.assignees
):
bulk_update_issue_assignees.append(
IssueAssignee(
issue=issue,
assignee_id=assignee_id,
created_by=request.user,
project_id=project_id,
workspace_id=workspace_id,
)
)
bulk_issue_activities.append(
{
"type": "issue.activity.updated",
"requested_data": json.dumps(
{
"assignee_ids": properties.get(
"assignee_ids", []
)
}
),
"current_instance": json.dumps(
{
"assignee_ids": [
str(assignee.id)
for assignee in issue.assignees.all()
]
}
),
"issue_id": str(issue.id),
"actor_id": str(request.user.id),
"project_id": str(project_id),
"epoch": epoch,
}
)
# Bulk update all the objects
Issue.objects.bulk_update(
bulk_update_issues,
[
"priority",
"start_date",
"target_date",
"state",
],
batch_size=100,
)
# Create new labels
IssueLabel.objects.bulk_create(
bulk_update_issue_labels,
ignore_conflicts=True,
batch_size=100,
)
# Create new assignees
IssueAssignee.objects.bulk_create(
bulk_update_issue_assignees,
ignore_conflicts=True,
batch_size=100,
)
# update the issue activity
[
issue_activity.delay(**activity)
for activity in bulk_issue_activities
]
return Response(status=status.HTTP_204_NO_CONTENT)
+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:
+64 -4
View File
@@ -52,7 +52,7 @@ from plane.db.models import (
)
class GlobalViewViewSet(BaseViewSet):
class WorkspaceViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer
model = IssueView
permission_classes = [
@@ -61,7 +61,7 @@ class GlobalViewViewSet(BaseViewSet):
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
serializer.save(workspace_id=workspace.id)
serializer.save(workspace_id=workspace.id, owned_by=self.request.user)
def get_queryset(self):
return self.filter_queryset(
@@ -69,13 +69,42 @@ class GlobalViewViewSet(BaseViewSet):
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__isnull=True)
.filter(Q(owned_by=self.request.user) | Q(access=1))
.select_related("workspace")
.order_by(self.request.GET.get("order_by", "-created_at"))
.distinct()
)
def partial_update(self, request, slug, pk):
workspace_view = IssueView.objects.get(
pk=pk,
workspace__slug=slug,
)
class GlobalViewIssuesViewSet(BaseViewSet):
if workspace_view.is_locked:
return Response(
{"error": "view is locked"},
status=status.HTTP_400_BAD_REQUEST,
)
# Only update the view if owner is updating
if workspace_view.owned_by_id != request.user.id:
return Response(
{"error": "Only the owner of the view can update the view"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = IssueViewSerializer(
workspace_view, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class WorkspaceViewIssuesViewSet(BaseViewSet):
permission_classes = [
WorkspaceEntityPermission,
]
@@ -271,7 +300,10 @@ class IssueViewViewSet(BaseViewSet):
]
def perform_create(self, serializer):
serializer.save(project_id=self.kwargs.get("project_id"))
serializer.save(
project_id=self.kwargs.get("project_id"),
owned_by=self.request.user,
)
def get_queryset(self):
subquery = UserFavorite.objects.filter(
@@ -291,6 +323,7 @@ class IssueViewViewSet(BaseViewSet):
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.filter(Q(owned_by=self.request.user) | Q(access=1))
.select_related("project")
.select_related("workspace")
.annotate(is_favorite=Exists(subquery))
@@ -310,6 +343,33 @@ class IssueViewViewSet(BaseViewSet):
).data
return Response(views, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk):
issue_view = IssueView.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
if issue_view.is_locked:
return Response(
{"error": "view is locked"},
status=status.HTTP_400_BAD_REQUEST,
)
# Only update the view if owner is updating
if issue_view.owned_by_id != request.user.id:
return Response(
{"error": "Only the owner of the view can update the view"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = IssueViewSerializer(
issue_view, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class IssueViewFavoriteViewSet(BaseViewSet):
model = UserFavorite
@@ -2,7 +2,6 @@
from django.db.models import (
Q,
Count,
Sum,
)
# Third party modules
@@ -87,29 +86,6 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
),
)
)
.annotate(
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.order_by(self.kwargs.get("order_by", "-created_at"))
.distinct()
)
@@ -20,7 +20,7 @@ from plane.authentication.utils.login import user_login
from plane.bgtasks.magic_link_code_task import magic_link
from plane.license.models import Instance
from plane.authentication.utils.host import base_host
from plane.db.models import User, Profile
from plane.db.models import User
from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
@@ -121,12 +121,7 @@ class MagicSignInSpaceEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# redirect to referer path
profile = Profile.objects.get(user=user)
if user.is_password_autoset and profile.is_onboarded:
path = "accounts/set-password"
else:
# Get the redirection path
path = str(next_path) if next_path else ""
path = str(next_path) if next_path else ""
url = f"{base_host(request=request, is_space=True)}{path}"
return HttpResponseRedirect(url)
@@ -1,6 +1,16 @@
# Generated by Django 4.2.11 on 2024-06-03 17:16
from django.db import migrations, models
from django.conf import settings
from django.db.models import F
import django.db.models.deletion
def populate_views_owned_by(apps, schema_editor):
IssueView = apps.get_model("db", "IssueView")
# update all existing views to be owned by the user who created them
IssueView.objects.update(owned_by_id=F("created_by_id"))
class Migration(migrations.Migration):
@@ -11,13 +21,53 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterField(
model_name='account',
name='provider',
field=models.CharField(choices=[('google', 'Google'), ('github', 'Github'), ('gitlab', 'GitLab')]),
model_name="account",
name="provider",
field=models.CharField(
choices=[
("google", "Google"),
("github", "Github"),
("gitlab", "GitLab"),
]
),
),
migrations.AlterField(
model_name='socialloginconnection',
name='medium',
field=models.CharField(choices=[('Google', 'google'), ('Github', 'github'), ('GitLab', 'gitlab'), ('Jira', 'jira')], default=None, max_length=20),
model_name="socialloginconnection",
name="medium",
field=models.CharField(
choices=[
("Google", "google"),
("Github", "github"),
("GitLab", "gitlab"),
("Jira", "jira"),
],
default=None,
max_length=20,
),
),
migrations.AddField(
model_name="issueview",
name="is_locked",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="issueview",
name="owned_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="views",
to=settings.AUTH_USER_MODEL,
null=True,
),
),
migrations.RunPython(populate_views_owned_by),
migrations.AlterField(
model_name="issueview",
name="owned_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="views",
to=settings.AUTH_USER_MODEL,
),
),
]
+1 -1
View File
@@ -65,7 +65,7 @@ from .session import Session
from .social_connection import SocialLoginConnection
from .state import State
from .user import Account, Profile, User
from .view import GlobalView, IssueView, IssueViewFavorite
from .view import IssueView, IssueViewFavorite
from .webhook import Webhook, WebhookLog
from .workspace import (
Team,
+7
View File
@@ -102,6 +102,13 @@ class IssueView(WorkspaceBaseModel):
)
sort_order = models.FloatField(default=65535)
logo_props = models.JSONField(default=dict)
owned_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="views",
)
is_locked = models.BooleanField(default=False)
class Meta:
verbose_name = "Issue View"
+1 -1
View File
@@ -101,7 +101,7 @@ def issue_on_results(issues, group_by, sub_group_by):
def issue_group_values(field, slug, project_id=None, filters=dict):
if field == "state_id":
queryset = State.objects.filter(
~Q(name="Triage"),
is_triage=False,
workspace__slug=slug,
).values_list("id", flat=True)
if project_id:
+12
View File
@@ -162,6 +162,8 @@ def filter_parent(params, filter, method, prefix=""):
parents = [
item for item in params.get("parent").split(",") if item != "null"
]
if "None" in parents:
filter[f"{prefix}parent__isnull"] = True
parents = filter_valid_uuids(parents)
if len(parents) and "" not in parents:
filter[f"{prefix}parent__in"] = parents
@@ -180,6 +182,8 @@ def filter_labels(params, filter, method, prefix=""):
labels = [
item for item in params.get("labels").split(",") if item != "null"
]
if "None" in labels:
filter[f"{prefix}labels__isnull"] = True
labels = filter_valid_uuids(labels)
if len(labels) and "" not in labels:
filter[f"{prefix}labels__in"] = labels
@@ -200,6 +204,8 @@ def filter_assignees(params, filter, method, prefix=""):
for item in params.get("assignees").split(",")
if item != "null"
]
if "None" in assignees:
filter[f"{prefix}assignees__isnull"] = True
assignees = filter_valid_uuids(assignees)
if len(assignees) and "" not in assignees:
filter[f"{prefix}assignees__in"] = assignees
@@ -242,6 +248,8 @@ def filter_created_by(params, filter, method, prefix=""):
for item in params.get("created_by").split(",")
if item != "null"
]
if "None" in created_bys:
filter[f"{prefix}created_by__isnull"] = True
created_bys = filter_valid_uuids(created_bys)
if len(created_bys) and "" not in created_bys:
filter[f"{prefix}created_by__in"] = created_bys
@@ -385,6 +393,8 @@ def filter_cycle(params, filter, method, prefix=""):
cycles = [
item for item in params.get("cycle").split(",") if item != "null"
]
if "None" in cycles:
filter[f"{prefix}issue_cycle__cycle_id__isnull"] = True
cycles = filter_valid_uuids(cycles)
if len(cycles) and "" not in cycles:
filter[f"{prefix}issue_cycle__cycle_id__in"] = cycles
@@ -403,6 +413,8 @@ def filter_module(params, filter, method, prefix=""):
modules = [
item for item in params.get("module").split(",") if item != "null"
]
if "None" in modules:
filter[f"{prefix}issue_module__module_id__isnull"] = True
modules = filter_valid_uuids(modules)
if len(modules) and "" not in modules:
filter[f"{prefix}issue_module__module_id__in"] = modules
+1 -1
View File
@@ -7,7 +7,7 @@
"web",
"space",
"admin",
"packages/editor/*",
"packages/editor",
"packages/eslint-config-custom",
"packages/tailwind-config-custom",
"packages/tsconfig",

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