Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b45d6d695d | |||
| 98b81d7ebb | |||
| 00807d2d62 | |||
| 9d757ccc6a | |||
| a8c253acfe | |||
| 2b106cbd66 | |||
| f9cca8e2cb | |||
| ee176efae3 | |||
| 44a483f895 | |||
| 196baf099a | |||
| 1a9ebc8b68 | |||
| 97e662215a | |||
| 606e34ec81 | |||
| a3019ebd46 | |||
| 9cfde896b3 | |||
| 4168127803 | |||
| 9dc14d8d67 | |||
| 56007e7d47 | |||
| cfb4a8212c | |||
| 9f41e92d21 | |||
| ddf07dc993 | |||
| eee9744159 | |||
| c80c76b882 | |||
| ffe38b592a | |||
| 87eb1949c6 | |||
| c736354739 | |||
| 3eecec552a | |||
| c561164d65 | |||
| c3dd790f7e | |||
| 70be4a4ace | |||
| 2c17f8ad72 | |||
| 151674687c | |||
| 8cd29c5009 | |||
| 9ce6179421 | |||
| 71ec9fadb7 | |||
| c88ed332e5 | |||
| d8c1dff34f | |||
| 5d161f671d | |||
| ae215a542e | |||
| b7c14ac9f5 | |||
| 75cd2017a0 | |||
| 1895cfe728 | |||
| 98e0089723 | |||
| 561ae82d9d | |||
| afac9f72db | |||
| 0f038705ed | |||
| 0b257c8693 | |||
| f2539c5051 | |||
| 5d60d6d702 | |||
| 3cbb60417c | |||
| d38f4811b6 | |||
| 3e82984f92 | |||
| 89ddd1bf42 | |||
| 0b15a32ec6 | |||
| f5eb13f66f | |||
| c3e8ce8f28 | |||
| 2ca8620246 | |||
| 07ff457663 | |||
| d3024f8a7f | |||
| a740a9f69d | |||
| 298acb1e91 | |||
| f9e21c58c0 | |||
| 5c842d592e | |||
| 0589ac56d5 | |||
| cbcb026e9a | |||
| 0cca31ef2e | |||
| 4c8f51568a | |||
| 8d14a5a004 | |||
| 97381c5145 | |||
| 4709a0fca4 | |||
| b695f161ef | |||
| 431af01483 | |||
| 269fc0d9e0 | |||
| 3c389e2e50 | |||
| f6677f252f | |||
| 794271ac17 | |||
| 931a48d7d7 | |||
| b37e453b32 | |||
| a69c6f1b9c | |||
| ee68895ad3 | |||
| e110ef55b8 | |||
| 5951372555 | |||
| 7ce21a6488 | |||
| 992457efd2 | |||
| c45151d5e6 | |||
| 726529044e | |||
| 0ad439fa63 | |||
| 843faf85b7 | |||
| ded36785e7 |
@@ -0,0 +1,7 @@
|
||||
[codespell]
|
||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
||||
skip = .git*,*.svg,i18n,*-lock.yaml,*.css,.codespellrc,migrations,*.js,*.map,*.mjs
|
||||
check-hidden = true
|
||||
# ignore all CamelCase and camelCase
|
||||
ignore-regex = \b[A-Za-z][a-z]+[A-Z][a-zA-Z]+\b
|
||||
ignore-words-list = tread
|
||||
@@ -61,3 +61,9 @@ temp/
|
||||
# Misc
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# React Router - https://github.com/remix-run/react-router-templates/blob/dc79b1a065f59f3bfd840d4ef75cc27689b611e6/default/.dockerignore
|
||||
.react-router/
|
||||
build/
|
||||
node_modules/
|
||||
README.md
|
||||
@@ -0,0 +1,25 @@
|
||||
# Codespell configuration is within .codespellrc
|
||||
---
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [preview]
|
||||
pull_request:
|
||||
branches: [preview]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Annotate locations with typos
|
||||
uses: codespell-project/codespell-problem-matcher@v1
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
+4
-1
@@ -16,9 +16,10 @@ node_modules
|
||||
/out/
|
||||
|
||||
# Production
|
||||
/build
|
||||
dist/
|
||||
out/
|
||||
build/
|
||||
.react-router/
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
@@ -99,3 +100,5 @@ dev-editor
|
||||
*.rdb.gz
|
||||
|
||||
storybook-static
|
||||
|
||||
CLAUDE.md
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[tools]
|
||||
node = "22.18.0"
|
||||
@@ -1,4 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
rules: {
|
||||
"no-duplicate-imports": "off",
|
||||
"import/no-duplicates": ["error", { "prefer-inline": false }],
|
||||
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
||||
"@typescript-eslint/no-import-type-side-effects": "error",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
prefer: "type-imports",
|
||||
fixStyle: "separate-type-imports",
|
||||
disallowTypeAnnotations: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Lightbulb } from "lucide-react";
|
||||
import { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@plane/types";
|
||||
// components
|
||||
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Artificial Intelligence Settings - God Mode",
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import Link from "next/link";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Monitor } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
|
||||
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { CodeBlock } from "@/components/common/code-block";
|
||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { CopyField, TCopyField } from "@/components/common/copy-field";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import type { TCopyField } from "@/components/common/copy-field";
|
||||
import { CopyField } from "@/components/common/copy-field";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
@@ -101,7 +107,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
||||
},
|
||||
];
|
||||
|
||||
const GITHUB_SERVICE_FIELD: TCopyField[] = [
|
||||
const GITHUB_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
||||
{
|
||||
key: "Origin_URL",
|
||||
label: "Origin URL",
|
||||
@@ -121,6 +127,9 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const GITHUB_SERVICE_DETAILS: TCopyField[] = [
|
||||
{
|
||||
key: "Callback_URI",
|
||||
label: "Callback URI",
|
||||
@@ -208,12 +217,29 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for GitHub</div>
|
||||
{GITHUB_SERVICE_FIELD.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for GitHub</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{/* common service details */}
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
|
||||
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* web service details */}
|
||||
<div className="flex flex-col rounded-lg overflow-hidden">
|
||||
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
|
||||
<Monitor className="w-3 h-3" />
|
||||
Web
|
||||
</div>
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
|
||||
{GITHUB_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "GitHub Authentication - God Mode",
|
||||
|
||||
@@ -6,7 +6,8 @@ import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// plane internal packages
|
||||
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { FC, useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import Link from "next/link";
|
||||
import { useForm } from "react-hook-form";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { CodeBlock } from "@/components/common/code-block";
|
||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { CopyField, TCopyField } from "@/components/common/copy-field";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import type { TCopyField } from "@/components/common/copy-field";
|
||||
import { CopyField } from "@/components/common/copy-field";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "GitLab Authentication - God Mode",
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import useSWR from "swr";
|
||||
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
// hooks
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
"use client";
|
||||
import { FC, useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import Link from "next/link";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Monitor } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { CodeBlock } from "@/components/common/code-block";
|
||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { CopyField, TCopyField } from "@/components/common/copy-field";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import type { TCopyField } from "@/components/common/copy-field";
|
||||
import { CopyField } from "@/components/common/copy-field";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
@@ -90,7 +95,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
||||
},
|
||||
];
|
||||
|
||||
const GOOGLE_SERVICE_DETAILS: TCopyField[] = [
|
||||
const GOOGLE_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
||||
{
|
||||
key: "Origin_URL",
|
||||
label: "Origin URL",
|
||||
@@ -110,6 +115,9 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
||||
</p>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const GOOGLE_SERVICE_DETAILS: TCopyField[] = [
|
||||
{
|
||||
key: "Callback_URI",
|
||||
label: "Callback URI",
|
||||
@@ -195,12 +203,29 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for Google</div>
|
||||
{GOOGLE_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for Google</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{/* common service details */}
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
|
||||
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* web service details */}
|
||||
<div className="flex flex-col rounded-lg overflow-hidden">
|
||||
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
|
||||
<Monitor className="w-3 h-3" />
|
||||
Web
|
||||
</div>
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
|
||||
{GOOGLE_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Google Authentication - God Mode",
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import useSWR from "swr";
|
||||
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
// hooks
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Authentication Settings - Plane Web",
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane internal packages
|
||||
import { TInstanceConfigurationKeys } from "@plane/types";
|
||||
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import type { TInstanceConfigurationKeys } from "@plane/types";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, useMemo, useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
// types
|
||||
import { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types";
|
||||
// ui
|
||||
import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { CustomSelect } from "@plane/ui";
|
||||
// components
|
||||
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// local components
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
interface EmailLayoutProps {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Loader, setToast, TOAST_TYPE, ToggleSwitch } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// components
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { InstanceService } from "@plane/services";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
import { Input } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Telescope } from "lucide-react";
|
||||
// types
|
||||
import { IInstance, IInstanceAdmin } from "@plane/types";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IInstance, IInstanceAdmin } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
||||
import { Input, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { IFormattedInstanceConfiguration } from "@plane/types";
|
||||
import type { IFormattedInstanceConfiguration } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "General Settings - God Mode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Menu, Settings } from "lucide-react";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from "@plane/types";
|
||||
// components
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
// hooks
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
interface ImageLayoutProps {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode, useEffect } from "react";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
// components
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useState, useRef } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { ExternalLink, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
// plane internal packages
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { DiscordIcon, GithubIcon } from "@plane/propel/icons";
|
||||
import { DiscordIcon, GithubIcon, PageIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
@@ -20,7 +21,7 @@ const helpOptions = [
|
||||
{
|
||||
name: "Documentation",
|
||||
href: "https://docs.plane.so/",
|
||||
Icon: FileText,
|
||||
Icon: PageIcon,
|
||||
},
|
||||
{
|
||||
name: "Join our Discord",
|
||||
@@ -110,7 +111,7 @@ export const AdminSidebarHelpSection: FC = observer(() => {
|
||||
<Link href={href} key={name} target="_blank">
|
||||
<div className="flex items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80">
|
||||
<div className="grid flex-shrink-0 place-items-center">
|
||||
<Icon className="h-3.5 w-3.5 text-custom-text-200" size={14} />
|
||||
<Icon className="h-3.5 w-3.5 text-custom-text-200" width={14} height={14} />
|
||||
</div>
|
||||
<span className="text-xs">{name}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
|
||||
@@ -4,10 +4,12 @@ import { useRouter } from "next/navigation";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// plane imports
|
||||
import { WEB_BASE_URL, ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { InstanceWorkspaceService } from "@plane/services";
|
||||
import { IWorkspace } from "@plane/types";
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
// components
|
||||
import { Button, CustomSelect, getButtonStyling, Input, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import { CustomSelect, Input } from "@plane/ui";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Workspace Management - God Mode",
|
||||
|
||||
@@ -6,8 +6,11 @@ import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
import { Loader as LoaderIcon } from "lucide-react";
|
||||
// types
|
||||
import { TInstanceConfigurationKeys } from "@plane/types";
|
||||
import { Button, getButtonStyling, Loader, setPromiseToast, ToggleSwitch } from "@plane/ui";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import type { TInstanceConfigurationKeys } from "@plane/types";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { WorkspaceListItem } from "@/components/workspace/list-item";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
import { Info, X } from "lucide-react";
|
||||
// plane constants
|
||||
import { TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
||||
|
||||
type TAuthBanner = {
|
||||
bannerData: TAdminAuthErrorInfo | undefined;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ReactNode } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { KeyRound, Mails } from "lucide-react";
|
||||
// plane packages
|
||||
import { SUPPORT_EMAIL, EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
|
||||
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import { SUPPORT_EMAIL, EAdminAuthErrorCodes } from "@plane/constants";
|
||||
import type { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL, EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import type { EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
import { Input, Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { Banner } from "@/components/common/banner";
|
||||
// local components
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, createContext } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createContext } from "react";
|
||||
// plane admin store
|
||||
import { RootStore } from "@/plane-admin/store/root.store";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toast } from "@plane/ui";
|
||||
import { Toast } from "@plane/propel/toast";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
|
||||
export const ToastWithTheme = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode, useEffect } from "react";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
// plane imports
|
||||
import { ADMIN_BASE_PATH } from "@plane/constants";
|
||||
// styles
|
||||
|
||||
@@ -3,7 +3,7 @@ import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { KeyRound, Mails } from "lucide-react";
|
||||
// types
|
||||
import {
|
||||
import type {
|
||||
TGetBaseAuthenticationModeProps,
|
||||
TInstanceAuthenticationMethodKeys,
|
||||
TInstanceAuthenticationModes,
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
// icons
|
||||
import { SquareArrowOutUpRight } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export const UpgradeButton: React.FC = () => (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// ui
|
||||
|
||||
@@ -6,8 +6,9 @@ import Link from "next/link";
|
||||
// icons
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
@@ -6,8 +6,9 @@ import Link from "next/link";
|
||||
// icons
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
@@ -6,8 +6,9 @@ import Link from "next/link";
|
||||
// icons
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// ui
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
||||
|
||||
type TBanner = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import Link from "next/link";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Controller, Control } from "react-hook-form";
|
||||
import type { Control } from "react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
// icons
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import React from "react";
|
||||
// ui
|
||||
import { Copy } from "lucide-react";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// icons
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button } from "@plane/propel/button";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button } from "@plane/propel/button";
|
||||
// assets
|
||||
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
||||
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button } from "@plane/propel/button";
|
||||
// assets
|
||||
import PlaneTakeOffImage from "@/public/images/plane-takeoff.png";
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
// icons
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Button, Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||
import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||
import { getPasswordStrength } from "@plane/utils";
|
||||
// components
|
||||
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
||||
|
||||
@@ -6,7 +6,7 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useTheme as nextUseTheme } from "next-themes";
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/(all)/store.provider";
|
||||
import { IInstanceStore } from "@/store/instance.store";
|
||||
import type { IInstanceStore } from "@/store/instance.store";
|
||||
|
||||
export const useInstance = (): IInstanceStore => {
|
||||
const context = useContext(StoreContext);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/(all)/store.provider";
|
||||
import { IThemeStore } from "@/store/theme.store";
|
||||
import type { IThemeStore } from "@/store/theme.store";
|
||||
|
||||
export const useTheme = (): IThemeStore => {
|
||||
const context = useContext(StoreContext);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/(all)/store.provider";
|
||||
import { IUserStore } from "@/store/user.store";
|
||||
import type { IUserStore } from "@/store/user.store";
|
||||
|
||||
export const useUser = (): IUserStore => {
|
||||
const context = useContext(StoreContext);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/(all)/store.provider";
|
||||
import { IWorkspaceStore } from "@/store/workspace.store";
|
||||
import type { IWorkspaceStore } from "@/store/workspace.store";
|
||||
|
||||
export const useWorkspace = (): IWorkspaceStore => {
|
||||
const context = useContext(StoreContext);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { set } from "lodash-es";
|
||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||
// plane internal packages
|
||||
import { EInstanceStatus, TInstanceStatus } from "@plane/constants";
|
||||
import type { TInstanceStatus } from "@plane/constants";
|
||||
import { EInstanceStatus } from "@plane/constants";
|
||||
import { InstanceService } from "@plane/services";
|
||||
import {
|
||||
import type {
|
||||
IInstance,
|
||||
IInstanceAdmin,
|
||||
IInstanceConfiguration,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
IInstanceConfig,
|
||||
} from "@plane/types";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
export interface IInstanceStore {
|
||||
// issues
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { enableStaticRendering } from "mobx-react";
|
||||
// stores
|
||||
import { IInstanceStore, InstanceStore } from "./instance.store";
|
||||
import { IThemeStore, ThemeStore } from "./theme.store";
|
||||
import { IUserStore, UserStore } from "./user.store";
|
||||
import { IWorkspaceStore, WorkspaceStore } from "./workspace.store";
|
||||
import type { IInstanceStore } from "./instance.store";
|
||||
import { InstanceStore } from "./instance.store";
|
||||
import type { IThemeStore } from "./theme.store";
|
||||
import { ThemeStore } from "./theme.store";
|
||||
import type { IUserStore } from "./user.store";
|
||||
import { UserStore } from "./user.store";
|
||||
import type { IWorkspaceStore } from "./workspace.store";
|
||||
import { WorkspaceStore } from "./workspace.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
type TTheme = "dark" | "light";
|
||||
export interface IThemeStore {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { action, observable, runInAction, makeObservable } from "mobx";
|
||||
// plane internal packages
|
||||
import { EUserStatus, TUserStatus } from "@plane/constants";
|
||||
import type { TUserStatus } from "@plane/constants";
|
||||
import { EUserStatus } from "@plane/constants";
|
||||
import { AuthService, UserService } from "@plane/services";
|
||||
import { IUser } from "@plane/types";
|
||||
import type { IUser } from "@plane/types";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
export interface IUserStore {
|
||||
// observables
|
||||
|
||||
@@ -2,9 +2,9 @@ import { set } from "lodash-es";
|
||||
import { action, observable, runInAction, makeObservable, computed } from "mobx";
|
||||
// plane imports
|
||||
import { InstanceWorkspaceService } from "@plane/services";
|
||||
import { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
|
||||
import type { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
export interface IWorkspaceStore {
|
||||
// observables
|
||||
|
||||
@@ -46,10 +46,9 @@
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash-es": "catalog:",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,27 @@
|
||||
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
|
||||
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
|
||||
--color-sidebar-shadow-4xl: var(--color-shadow-4xl);
|
||||
|
||||
/* toast theme */
|
||||
--color-toast-success-text: 178, 221, 181;
|
||||
--color-toast-error-text: 206, 44, 49;
|
||||
--color-toast-warning-text: 255, 186, 24;
|
||||
--color-toast-info-text: 141, 164, 239;
|
||||
--color-toast-loading-text: 255, 255, 255;
|
||||
--color-toast-secondary-text: 185, 187, 198;
|
||||
--color-toast-tertiary-text: 139, 141, 152;
|
||||
|
||||
--color-toast-success-background: 46, 46, 46;
|
||||
--color-toast-error-background: 46, 46, 46;
|
||||
--color-toast-warning-background: 46, 46, 46;
|
||||
--color-toast-info-background: 46, 46, 46;
|
||||
--color-toast-loading-background: 46, 46, 46;
|
||||
|
||||
--color-toast-success-border: 42, 126, 59;
|
||||
--color-toast-error-border: 100, 23, 35;
|
||||
--color-toast-warning-border: 79, 52, 34;
|
||||
--color-toast-info-border: 58, 91, 199;
|
||||
--color-toast-loading-border: 96, 100, 108;
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
@@ -221,27 +242,6 @@
|
||||
--color-border-200: 38, 38, 38; /* subtle border- 2 */
|
||||
--color-border-300: 46, 46, 46; /* strong border- 1 */
|
||||
--color-border-400: 58, 58, 58; /* strong border- 2 */
|
||||
|
||||
/* toast theme */
|
||||
--color-toast-success-text: 178, 221, 181;
|
||||
--color-toast-error-text: 206, 44, 49;
|
||||
--color-toast-warning-text: 255, 186, 24;
|
||||
--color-toast-info-text: 141, 164, 239;
|
||||
--color-toast-loading-text: 255, 255, 255;
|
||||
--color-toast-secondary-text: 185, 187, 198;
|
||||
--color-toast-tertiary-text: 139, 141, 152;
|
||||
|
||||
--color-toast-success-background: 46, 46, 46;
|
||||
--color-toast-error-background: 46, 46, 46;
|
||||
--color-toast-warning-background: 46, 46, 46;
|
||||
--color-toast-info-background: 46, 46, 46;
|
||||
--color-toast-loading-background: 46, 46, 46;
|
||||
|
||||
--color-toast-success-border: 42, 126, 59;
|
||||
--color-toast-error-border: 100, 23, 35;
|
||||
--color-toast-warning-border: 79, 52, 34;
|
||||
--color-toast-info-border: 58, 91, 199;
|
||||
--color-toast-loading-border: 96, 100, 108;
|
||||
}
|
||||
|
||||
[data-theme="dark-contrast"] {
|
||||
|
||||
@@ -4,7 +4,7 @@ from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import Cycle, CycleIssue
|
||||
from plane.db.models import Cycle, CycleIssue, User
|
||||
from plane.utils.timezone_converter import convert_to_utc
|
||||
|
||||
|
||||
@@ -16,6 +16,13 @@ class CycleCreateSerializer(BaseSerializer):
|
||||
and UTC normalization for time-bound iteration planning and sprint management.
|
||||
"""
|
||||
|
||||
owned_by = serializers.PrimaryKeyRelatedField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text="User who owns the cycle. If not provided, defaults to the current user.",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
project = self.context.get("project")
|
||||
@@ -72,6 +79,10 @@ class CycleCreateSerializer(BaseSerializer):
|
||||
date=str(data.get("end_date", None).date()),
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
if not data.get("owned_by"):
|
||||
data["owned_by"] = self.context["request"].user
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Module improts
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .issue import IssueExpandSerializer
|
||||
from plane.db.models import IntakeIssue, Issue
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
from .asset import urlpatterns as asset_patterns
|
||||
from .cycle import urlpatterns as cycle_patterns
|
||||
from .intake import urlpatterns as intake_patterns
|
||||
from .label import urlpatterns as label_patterns
|
||||
from .member import urlpatterns as member_patterns
|
||||
from .module import urlpatterns as module_patterns
|
||||
from .project import urlpatterns as project_patterns
|
||||
from .state import urlpatterns as state_patterns
|
||||
from .issue import urlpatterns as issue_patterns
|
||||
from .cycle import urlpatterns as cycle_patterns
|
||||
from .module import urlpatterns as module_patterns
|
||||
from .intake import urlpatterns as intake_patterns
|
||||
from .member import urlpatterns as member_patterns
|
||||
from .asset import urlpatterns as asset_patterns
|
||||
from .user import urlpatterns as user_patterns
|
||||
from .work_item import urlpatterns as work_item_patterns
|
||||
|
||||
urlpatterns = [
|
||||
*asset_patterns,
|
||||
*cycle_patterns,
|
||||
*intake_patterns,
|
||||
*label_patterns,
|
||||
*member_patterns,
|
||||
*module_patterns,
|
||||
*project_patterns,
|
||||
*state_patterns,
|
||||
*issue_patterns,
|
||||
*cycle_patterns,
|
||||
*module_patterns,
|
||||
*intake_patterns,
|
||||
*member_patterns,
|
||||
*user_patterns,
|
||||
*work_item_patterns,
|
||||
]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import LabelListCreateAPIEndpoint, LabelDetailAPIEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/",
|
||||
LabelListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="label",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/<uuid:pk>/",
|
||||
LabelDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="label",
|
||||
),
|
||||
]
|
||||
@@ -3,8 +3,6 @@ from django.urls import path
|
||||
from plane.api.views import (
|
||||
IssueListCreateAPIEndpoint,
|
||||
IssueDetailAPIEndpoint,
|
||||
LabelListCreateAPIEndpoint,
|
||||
LabelDetailAPIEndpoint,
|
||||
IssueLinkListCreateAPIEndpoint,
|
||||
IssueLinkDetailAPIEndpoint,
|
||||
IssueCommentListCreateAPIEndpoint,
|
||||
@@ -17,7 +15,8 @@ from plane.api.views import (
|
||||
IssueSearchEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# Deprecated url patterns
|
||||
old_url_patterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/issues/search/",
|
||||
IssueSearchEndpoint.as_view(http_method_names=["get"]),
|
||||
@@ -38,16 +37,6 @@ urlpatterns = [
|
||||
IssueDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/",
|
||||
LabelListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="label",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/<uuid:pk>/",
|
||||
LabelDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="label",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/links/",
|
||||
IssueLinkListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
@@ -89,3 +78,69 @@ urlpatterns = [
|
||||
name="issue-attachment",
|
||||
),
|
||||
]
|
||||
|
||||
# New url patterns with work-items as the prefix
|
||||
new_url_patterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/work-items/search/",
|
||||
IssueSearchEndpoint.as_view(http_method_names=["get"]),
|
||||
name="work-item-search",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/work-items/<str:project_identifier>-<str:issue_identifier>/",
|
||||
WorkspaceIssueAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
name="work-item-by-identifier",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/",
|
||||
IssueListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="work-item-list",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:pk>/",
|
||||
IssueDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="work-item-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/links/",
|
||||
IssueLinkListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="work-item-link-list",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/links/<uuid:pk>/",
|
||||
IssueLinkDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="work-item-link-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/comments/",
|
||||
IssueCommentListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="work-item-comment-list",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||
IssueCommentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="work-item-comment-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/activities/",
|
||||
IssueActivityListAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
name="work-item-activity-list",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/activities/<uuid:pk>/",
|
||||
IssueActivityDetailAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
name="work-item-activity-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/attachments/",
|
||||
IssueAttachmentListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="work-item-attachment-list",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/attachments/<uuid:pk>/",
|
||||
IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="work-item-attachment-detail",
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns = old_url_patterns + new_url_patterns
|
||||
@@ -307,7 +307,8 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or (
|
||||
request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None
|
||||
):
|
||||
serializer = CycleCreateSerializer(data=request.data)
|
||||
|
||||
serializer = CycleCreateSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
@@ -332,7 +333,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
serializer.save(project_id=project_id, owned_by=request.user)
|
||||
serializer.save(project_id=project_id)
|
||||
# Send the model activity
|
||||
model_activity.delay(
|
||||
model_name="cycle",
|
||||
@@ -518,7 +519,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CycleUpdateSerializer(cycle, data=request.data, partial=True)
|
||||
serializer = CycleUpdateSerializer(cycle, data=request.data, partial=True, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
|
||||
@@ -79,7 +79,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
| Q(network=2)
|
||||
)
|
||||
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
|
||||
.select_related("project_lead")
|
||||
.annotate(
|
||||
is_member=Exists(
|
||||
ProjectMember.objects.filter(
|
||||
|
||||
@@ -1196,7 +1196,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
if the issues were transferred to the new cycle, then the progress_snapshot will be present
|
||||
return the progress_snapshot data in the analytics for each date
|
||||
|
||||
else issues were not transferred to the new cycle then generate the stats from the cycle isssue bridge tables
|
||||
else issues were not transferred to the new cycle then generate the stats from the cycle issue bridge tables
|
||||
"""
|
||||
|
||||
if cycle.progress_snapshot:
|
||||
|
||||
@@ -48,7 +48,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
# Check if the members array is empty
|
||||
if not len(members):
|
||||
return Response(
|
||||
{"error": "Atleast one member is required"},
|
||||
{"error": "At least one member is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class TimezoneEndpoint(APIView):
|
||||
("Tunis", "Africa/Tunis"), # UTC+01:00
|
||||
(
|
||||
"Eastern European Time (Cairo, Helsinki, Kyiv)",
|
||||
"Europe/Kiev",
|
||||
"Europe/Kyiv",
|
||||
), # UTC+02:00 (DST: UTC+03:00)
|
||||
("Athens", "Europe/Athens"), # UTC+02:00 (DST: UTC+03:00)
|
||||
("Jerusalem", "Asia/Jerusalem"), # UTC+02:00 (DST: UTC+03:00)
|
||||
|
||||
@@ -48,7 +48,7 @@ AUTHENTICATION_ERROR_CODES = {
|
||||
"INCORRECT_OLD_PASSWORD": 5135,
|
||||
"MISSING_PASSWORD": 5138,
|
||||
"INVALID_NEW_PASSWORD": 5140,
|
||||
# set passowrd
|
||||
# set password
|
||||
"PASSWORD_ALREADY_SET": 5145,
|
||||
# Admin
|
||||
"ADMIN_ALREADY_EXIST": 5150,
|
||||
|
||||
@@ -410,7 +410,8 @@ def get_webhook_logs_queryset():
|
||||
"response_headers",
|
||||
"retry_count",
|
||||
)
|
||||
.iterator(chunk_size=BATCH_SIZE)
|
||||
.order_by("created_at")
|
||||
.iterator(chunk_size=100)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,82 +1,24 @@
|
||||
# Python imports
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import zipfile
|
||||
from typing import List
|
||||
from collections import defaultdict
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
from uuid import UUID
|
||||
from datetime import datetime, date
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from openpyxl import Workbook
|
||||
from django.db.models import F, Prefetch
|
||||
|
||||
from collections import defaultdict
|
||||
from django.db.models import Prefetch
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import ExporterHistory, Issue, FileAsset, Label, User, IssueComment
|
||||
from plane.db.models import ExporterHistory, Issue, IssueRelation
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
def dateTimeConverter(time: datetime) -> str | None:
|
||||
"""
|
||||
Convert a datetime object to a formatted string.
|
||||
"""
|
||||
if time:
|
||||
return time.strftime("%a, %d %b %Y %I:%M:%S %Z%z")
|
||||
|
||||
|
||||
def dateConverter(time: date) -> str | None:
|
||||
"""
|
||||
Convert a date object to a formatted string.
|
||||
"""
|
||||
if time:
|
||||
return time.strftime("%a, %d %b %Y")
|
||||
|
||||
|
||||
def create_csv_file(data: List[List[str]]) -> str:
|
||||
"""
|
||||
Create a CSV file from the provided data.
|
||||
"""
|
||||
csv_buffer = io.StringIO()
|
||||
csv_writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||
|
||||
for row in data:
|
||||
csv_writer.writerow(row)
|
||||
|
||||
csv_buffer.seek(0)
|
||||
return csv_buffer.getvalue()
|
||||
|
||||
|
||||
def create_json_file(data: List[dict]) -> str:
|
||||
"""
|
||||
Create a JSON file from the provided data.
|
||||
"""
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
def create_xlsx_file(data: List[List[str]]) -> bytes:
|
||||
"""
|
||||
Create an XLSX file from the provided data.
|
||||
"""
|
||||
workbook = Workbook()
|
||||
sheet = workbook.active
|
||||
|
||||
for row in data:
|
||||
sheet.append(row)
|
||||
|
||||
xlsx_buffer = io.BytesIO()
|
||||
workbook.save(xlsx_buffer)
|
||||
xlsx_buffer.seek(0)
|
||||
return xlsx_buffer.getvalue()
|
||||
from plane.utils.exporters import Exporter, IssueExportSchema
|
||||
|
||||
|
||||
def create_zip_file(files: List[tuple[str, str | bytes]]) -> io.BytesIO:
|
||||
@@ -118,7 +60,9 @@ def upload_to_s3(zip_file: io.BytesIO, workspace_id: UUID, token_id: str, slug:
|
||||
# Generate presigned url for the uploaded file with different base
|
||||
presign_s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=f"{settings.AWS_S3_URL_PROTOCOL}//{str(settings.AWS_S3_CUSTOM_DOMAIN).replace('/uploads', '')}/", # noqa: E501
|
||||
endpoint_url=(
|
||||
f"{settings.AWS_S3_URL_PROTOCOL}//{str(settings.AWS_S3_CUSTOM_DOMAIN).replace('/uploads', '')}/"
|
||||
),
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
@@ -176,187 +120,6 @@ def upload_to_s3(zip_file: io.BytesIO, workspace_id: UUID, token_id: str, slug:
|
||||
exporter_instance.save(update_fields=["status", "url", "key"])
|
||||
|
||||
|
||||
def generate_table_row(issue: dict) -> List[str]:
|
||||
"""
|
||||
Generate a table row from an issue dictionary.
|
||||
"""
|
||||
return [
|
||||
f"""{issue["project_identifier"]}-{issue["sequence_id"]}""",
|
||||
issue["project_name"],
|
||||
issue["name"],
|
||||
issue["description"],
|
||||
issue["state_name"],
|
||||
dateConverter(issue["start_date"]),
|
||||
dateConverter(issue["target_date"]),
|
||||
issue["priority"],
|
||||
issue["created_by"],
|
||||
", ".join(issue["labels"]) if issue["labels"] else "",
|
||||
issue["cycle_name"],
|
||||
issue["cycle_start_date"],
|
||||
issue["cycle_end_date"],
|
||||
", ".join(issue.get("module_name", "")) if issue.get("module_name") else "",
|
||||
dateTimeConverter(issue["created_at"]),
|
||||
dateTimeConverter(issue["updated_at"]),
|
||||
dateTimeConverter(issue["completed_at"]),
|
||||
dateTimeConverter(issue["archived_at"]),
|
||||
(
|
||||
", ".join(
|
||||
[
|
||||
f"{comment['comment']} ({comment['created_at']} by {comment['created_by']})"
|
||||
for comment in issue["comments"]
|
||||
]
|
||||
)
|
||||
if issue["comments"]
|
||||
else ""
|
||||
),
|
||||
issue["estimate"] if issue["estimate"] else "",
|
||||
", ".join(issue["link"]) if issue["link"] else "",
|
||||
", ".join(issue["assignees"]) if issue["assignees"] else "",
|
||||
issue["subscribers_count"] if issue["subscribers_count"] else "",
|
||||
issue["attachment_count"] if issue["attachment_count"] else "",
|
||||
", ".join(issue["attachment_links"]) if issue["attachment_links"] else "",
|
||||
]
|
||||
|
||||
|
||||
def generate_json_row(issue: dict) -> dict:
|
||||
"""
|
||||
Generate a JSON row from an issue dictionary.
|
||||
"""
|
||||
return {
|
||||
"ID": f"""{issue["project_identifier"]}-{issue["sequence_id"]}""",
|
||||
"Project": issue["project_name"],
|
||||
"Name": issue["name"],
|
||||
"Description": issue["description"],
|
||||
"State": issue["state_name"],
|
||||
"Start Date": dateConverter(issue["start_date"]),
|
||||
"Target Date": dateConverter(issue["target_date"]),
|
||||
"Priority": issue["priority"],
|
||||
"Created By": (f"{issue['created_by']}" if issue["created_by"] else ""),
|
||||
"Assignee": issue["assignees"],
|
||||
"Labels": issue["labels"],
|
||||
"Cycle Name": issue["cycle_name"],
|
||||
"Cycle Start Date": issue["cycle_start_date"],
|
||||
"Cycle End Date": issue["cycle_end_date"],
|
||||
"Module Name": issue["module_name"],
|
||||
"Created At": dateTimeConverter(issue["created_at"]),
|
||||
"Updated At": dateTimeConverter(issue["updated_at"]),
|
||||
"Completed At": dateTimeConverter(issue["completed_at"]),
|
||||
"Archived At": dateTimeConverter(issue["archived_at"]),
|
||||
"Comments": issue["comments"],
|
||||
"Estimate": issue["estimate"],
|
||||
"Link": issue["link"],
|
||||
"Subscribers Count": issue["subscribers_count"],
|
||||
"Attachment Count": issue["attachment_count"],
|
||||
"Attachment Links": issue["attachment_links"],
|
||||
}
|
||||
|
||||
|
||||
def update_json_row(rows: List[dict], row: dict) -> None:
|
||||
"""
|
||||
Update the json row with the new assignee and label.
|
||||
"""
|
||||
matched_index = next(
|
||||
(index for index, existing_row in enumerate(rows) if existing_row["ID"] == row["ID"]),
|
||||
None,
|
||||
)
|
||||
|
||||
if matched_index is not None:
|
||||
existing_assignees, existing_labels = (
|
||||
rows[matched_index]["Assignee"],
|
||||
rows[matched_index]["Labels"],
|
||||
)
|
||||
assignee, label = row["Assignee"], row["Labels"]
|
||||
|
||||
if assignee is not None and (existing_assignees is None or label not in existing_assignees):
|
||||
rows[matched_index]["Assignee"] += f", {assignee}"
|
||||
if label is not None and (existing_labels is None or label not in existing_labels):
|
||||
rows[matched_index]["Labels"] += f", {label}"
|
||||
else:
|
||||
rows.append(row)
|
||||
|
||||
|
||||
def update_table_row(rows: List[List[str]], row: List[str]) -> None:
|
||||
"""
|
||||
Update the table row with the new assignee and label.
|
||||
"""
|
||||
matched_index = next(
|
||||
(index for index, existing_row in enumerate(rows) if existing_row[0] == row[0]),
|
||||
None,
|
||||
)
|
||||
|
||||
if matched_index is not None:
|
||||
existing_assignees, existing_labels = rows[matched_index][7:9]
|
||||
assignee, label = row[7:9]
|
||||
|
||||
if assignee is not None and (existing_assignees is None or label not in existing_assignees):
|
||||
rows[matched_index][8] += f", {assignee}"
|
||||
if label is not None and (existing_labels is None or label not in existing_labels):
|
||||
rows[matched_index][8] += f", {label}"
|
||||
else:
|
||||
rows.append(row)
|
||||
|
||||
|
||||
def generate_csv(
|
||||
header: List[str],
|
||||
project_id: str,
|
||||
issues: List[dict],
|
||||
files: List[tuple[str, str | bytes]],
|
||||
) -> None:
|
||||
"""
|
||||
Generate CSV export for all the passed issues.
|
||||
"""
|
||||
rows = [header]
|
||||
for issue in issues:
|
||||
row = generate_table_row(issue)
|
||||
update_table_row(rows, row)
|
||||
csv_file = create_csv_file(rows)
|
||||
files.append((f"{project_id}.csv", csv_file))
|
||||
|
||||
|
||||
def generate_json(
|
||||
header: List[str],
|
||||
project_id: str,
|
||||
issues: List[dict],
|
||||
files: List[tuple[str, str | bytes]],
|
||||
) -> None:
|
||||
"""
|
||||
Generate JSON export for all the passed issues.
|
||||
"""
|
||||
rows = []
|
||||
for issue in issues:
|
||||
row = generate_json_row(issue)
|
||||
update_json_row(rows, row)
|
||||
json_file = create_json_file(rows)
|
||||
files.append((f"{project_id}.json", json_file))
|
||||
|
||||
|
||||
def generate_xlsx(
|
||||
header: List[str],
|
||||
project_id: str,
|
||||
issues: List[dict],
|
||||
files: List[tuple[str, str | bytes]],
|
||||
) -> None:
|
||||
"""
|
||||
Generate XLSX export for all the passed issues.
|
||||
"""
|
||||
rows = [header]
|
||||
for issue in issues:
|
||||
row = generate_table_row(issue)
|
||||
|
||||
update_table_row(rows, row)
|
||||
xlsx_file = create_xlsx_file(rows)
|
||||
files.append((f"{project_id}.xlsx", xlsx_file))
|
||||
|
||||
|
||||
def get_created_by(obj: Issue | IssueComment) -> str:
|
||||
"""
|
||||
Get the created by user for the given object.
|
||||
"""
|
||||
if obj.created_by:
|
||||
return f"{obj.created_by.first_name} {obj.created_by.last_name}"
|
||||
return ""
|
||||
|
||||
|
||||
@shared_task
|
||||
def issue_export_task(
|
||||
provider: str,
|
||||
@@ -377,7 +140,7 @@ def issue_export_task(
|
||||
exporter_instance.status = "processing"
|
||||
exporter_instance.save(update_fields=["status"])
|
||||
|
||||
# Base query to get the issues
|
||||
# Build base queryset for issues
|
||||
workspace_issues = (
|
||||
Issue.objects.filter(
|
||||
workspace__id=workspace_id,
|
||||
@@ -390,7 +153,6 @@ def issue_export_task(
|
||||
"project",
|
||||
"workspace",
|
||||
"state",
|
||||
"parent",
|
||||
"created_by",
|
||||
"estimate_point",
|
||||
)
|
||||
@@ -400,144 +162,51 @@ def issue_export_task(
|
||||
"issue_module__module",
|
||||
"issue_comments",
|
||||
"assignees",
|
||||
Prefetch(
|
||||
"assignees",
|
||||
queryset=User.objects.only("first_name", "last_name").distinct(),
|
||||
to_attr="assignee_details",
|
||||
),
|
||||
Prefetch(
|
||||
"labels",
|
||||
queryset=Label.objects.only("name").distinct(),
|
||||
to_attr="label_details",
|
||||
),
|
||||
"issue_subscribers",
|
||||
"issue_link",
|
||||
Prefetch(
|
||||
"issue_relation",
|
||||
queryset=IssueRelation.objects.select_related("related_issue", "related_issue__project"),
|
||||
),
|
||||
Prefetch(
|
||||
"issue_related",
|
||||
queryset=IssueRelation.objects.select_related("issue", "issue__project"),
|
||||
),
|
||||
Prefetch(
|
||||
"parent",
|
||||
queryset=Issue.objects.select_related("type", "project"),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Get the attachments for the issues
|
||||
file_assets = FileAsset.objects.filter(
|
||||
issue_id__in=workspace_issues.values_list("id", flat=True),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
).annotate(work_item_id=F("issue_id"), asset_id=F("id"))
|
||||
|
||||
# Create a dictionary to store the attachments for the issues
|
||||
attachment_dict = defaultdict(list)
|
||||
for asset in file_assets:
|
||||
attachment_dict[asset.work_item_id].append(asset.asset_id)
|
||||
|
||||
# Create a list to store the issues data
|
||||
issues_data = []
|
||||
|
||||
# Iterate over the issues
|
||||
for issue in workspace_issues:
|
||||
attachments = attachment_dict.get(issue.id, [])
|
||||
|
||||
issue_data = {
|
||||
"id": issue.id,
|
||||
"project_identifier": issue.project.identifier,
|
||||
"project_name": issue.project.name,
|
||||
"project_id": issue.project.id,
|
||||
"sequence_id": issue.sequence_id,
|
||||
"name": issue.name,
|
||||
"description": issue.description_stripped,
|
||||
"priority": issue.priority,
|
||||
"start_date": issue.start_date,
|
||||
"target_date": issue.target_date,
|
||||
"state_name": issue.state.name if issue.state else None,
|
||||
"created_at": issue.created_at,
|
||||
"updated_at": issue.updated_at,
|
||||
"completed_at": issue.completed_at,
|
||||
"archived_at": issue.archived_at,
|
||||
"module_name": [module.module.name for module in issue.issue_module.all()],
|
||||
"created_by": get_created_by(issue),
|
||||
"labels": [label.name for label in issue.label_details],
|
||||
"comments": [
|
||||
{
|
||||
"comment": comment.comment_stripped,
|
||||
"created_at": dateConverter(comment.created_at),
|
||||
"created_by": get_created_by(comment),
|
||||
}
|
||||
for comment in issue.issue_comments.all()
|
||||
],
|
||||
"estimate": issue.estimate_point.value if issue.estimate_point and issue.estimate_point.value else "",
|
||||
"link": [link.url for link in issue.issue_link.all()],
|
||||
"assignees": [f"{assignee.first_name} {assignee.last_name}" for assignee in issue.assignee_details],
|
||||
"subscribers_count": issue.issue_subscribers.count(),
|
||||
"attachment_count": len(attachments),
|
||||
"attachment_links": [
|
||||
f"/api/assets/v2/workspaces/{issue.workspace.slug}/projects/{issue.project_id}/issues/{issue.id}/attachments/{asset}/"
|
||||
for asset in attachments
|
||||
],
|
||||
}
|
||||
|
||||
# Get Cycles data for the issue
|
||||
cycle = issue.issue_cycle.last()
|
||||
if cycle:
|
||||
# Update cycle data
|
||||
issue_data["cycle_name"] = cycle.cycle.name
|
||||
issue_data["cycle_start_date"] = dateConverter(cycle.cycle.start_date)
|
||||
issue_data["cycle_end_date"] = dateConverter(cycle.cycle.end_date)
|
||||
else:
|
||||
issue_data["cycle_name"] = ""
|
||||
issue_data["cycle_start_date"] = ""
|
||||
issue_data["cycle_end_date"] = ""
|
||||
|
||||
issues_data.append(issue_data)
|
||||
|
||||
# CSV header
|
||||
header = [
|
||||
"ID",
|
||||
"Project",
|
||||
"Name",
|
||||
"Description",
|
||||
"State",
|
||||
"Start Date",
|
||||
"Target Date",
|
||||
"Priority",
|
||||
"Created By",
|
||||
"Labels",
|
||||
"Cycle Name",
|
||||
"Cycle Start Date",
|
||||
"Cycle End Date",
|
||||
"Module Name",
|
||||
"Created At",
|
||||
"Updated At",
|
||||
"Completed At",
|
||||
"Archived At",
|
||||
"Comments",
|
||||
"Estimate",
|
||||
"Link",
|
||||
"Assignees",
|
||||
"Subscribers Count",
|
||||
"Attachment Count",
|
||||
"Attachment Links",
|
||||
]
|
||||
|
||||
# Map the provider to the function
|
||||
EXPORTER_MAPPER = {
|
||||
"csv": generate_csv,
|
||||
"json": generate_json,
|
||||
"xlsx": generate_xlsx,
|
||||
}
|
||||
# Create exporter for the specified format
|
||||
try:
|
||||
exporter = Exporter(
|
||||
format_type=provider,
|
||||
schema_class=IssueExportSchema,
|
||||
options={"list_joiner": ", "},
|
||||
)
|
||||
except ValueError as e:
|
||||
# Invalid format type
|
||||
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||
exporter_instance.status = "failed"
|
||||
exporter_instance.reason = str(e)
|
||||
exporter_instance.save(update_fields=["status", "reason"])
|
||||
return
|
||||
|
||||
files = []
|
||||
if multiple:
|
||||
project_dict = defaultdict(list)
|
||||
for issue in issues_data:
|
||||
project_dict[str(issue["project_id"])].append(issue)
|
||||
|
||||
# Export each project separately with its own queryset
|
||||
for project_id in project_ids:
|
||||
issues = project_dict.get(str(project_id), [])
|
||||
|
||||
exporter = EXPORTER_MAPPER.get(provider)
|
||||
if exporter is not None:
|
||||
exporter(header, project_id, issues, files)
|
||||
|
||||
project_issues = workspace_issues.filter(project_id=project_id)
|
||||
export_filename = f"{slug}-{project_id}"
|
||||
filename, content = exporter.export(export_filename, project_issues)
|
||||
files.append((filename, content))
|
||||
else:
|
||||
exporter = EXPORTER_MAPPER.get(provider)
|
||||
if exporter is not None:
|
||||
exporter(header, workspace_id, issues_data, files)
|
||||
# Export all issues in a single file
|
||||
export_filename = f"{slug}-{workspace_id}"
|
||||
filename, content = exporter.export(export_filename, workspace_issues)
|
||||
files.append((filename, content))
|
||||
|
||||
zip_buffer = create_zip_file(files)
|
||||
upload_to_s3(zip_buffer, workspace_id, token_id, slug)
|
||||
|
||||
@@ -1154,7 +1154,10 @@ def create_comment_reaction_activity(
|
||||
.values_list("id", "comment__id")
|
||||
.first()
|
||||
)
|
||||
comment = IssueComment.objects.get(pk=comment_id, project_id=project_id)
|
||||
comment = IssueComment.objects.filter(pk=comment_id, project_id=project_id).first()
|
||||
if comment is None:
|
||||
return
|
||||
|
||||
if comment is not None and comment_reaction_id is not None and comment_id is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
|
||||
@@ -48,6 +48,8 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.settings.mongo import MongoConnection
|
||||
|
||||
|
||||
SERIALIZER_MAPPER = {
|
||||
"project": ProjectSerializer,
|
||||
@@ -84,6 +86,58 @@ def get_issue_prefetches():
|
||||
]
|
||||
|
||||
|
||||
|
||||
def save_webhook_log(
|
||||
webhook: Webhook,
|
||||
request_method: str,
|
||||
request_headers: str,
|
||||
request_body: str,
|
||||
response_status: str,
|
||||
response_headers: str,
|
||||
response_body: str,
|
||||
retry_count: int,
|
||||
event_type: str,
|
||||
) -> None:
|
||||
|
||||
# webhook_logs
|
||||
mongo_collection = MongoConnection.get_collection("webhook_logs")
|
||||
|
||||
log_data = {
|
||||
"workspace_id": str(webhook.workspace_id),
|
||||
"webhook": str(webhook.id),
|
||||
"event_type": str(event_type),
|
||||
"request_method": str(request_method),
|
||||
"request_headers": str(request_headers),
|
||||
"request_body": str(request_body),
|
||||
"response_status": str(response_status),
|
||||
"response_headers": str(response_headers),
|
||||
"response_body": str(response_body),
|
||||
"retry_count": retry_count,
|
||||
}
|
||||
|
||||
mongo_save_success = False
|
||||
if mongo_collection is not None:
|
||||
try:
|
||||
# insert the log data into the mongo collection
|
||||
mongo_collection.insert_one(log_data)
|
||||
logger.info("Webhook log saved successfully to mongo")
|
||||
mongo_save_success = True
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
logger.error(f"Failed to save webhook log: {e}")
|
||||
mongo_save_success = False
|
||||
|
||||
# if the mongo save is not successful, save the log data into the database
|
||||
if not mongo_save_success:
|
||||
try:
|
||||
# insert the log data into the database
|
||||
WebhookLog.objects.create(**log_data)
|
||||
logger.info("Webhook log saved successfully to database")
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
logger.error(f"Failed to save webhook log: {e}")
|
||||
|
||||
|
||||
def get_model_data(event: str, event_id: Union[str, List[str]], many: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve and serialize model data based on the event type.
|
||||
@@ -273,32 +327,30 @@ def webhook_send_task(
|
||||
response = requests.post(webhook.url, headers=headers, json=payload, timeout=30)
|
||||
|
||||
# Log the webhook request
|
||||
WebhookLog.objects.create(
|
||||
workspace_id=str(webhook.workspace_id),
|
||||
webhook=str(webhook.id),
|
||||
event_type=str(event),
|
||||
request_method=str(action),
|
||||
request_headers=str(headers),
|
||||
request_body=str(payload),
|
||||
response_status=str(response.status_code),
|
||||
response_headers=str(response.headers),
|
||||
response_body=str(response.text),
|
||||
retry_count=str(self.request.retries),
|
||||
save_webhook_log(
|
||||
webhook=webhook,
|
||||
request_method=action,
|
||||
request_headers=headers,
|
||||
request_body=payload,
|
||||
response_status=response.status_code,
|
||||
response_headers=response.headers,
|
||||
response_body=response.text,
|
||||
retry_count=self.request.retries,
|
||||
event_type=event,
|
||||
)
|
||||
logger.info(f"Webhook {webhook.id} sent successfully")
|
||||
except requests.RequestException as e:
|
||||
# Log the failed webhook request
|
||||
WebhookLog.objects.create(
|
||||
workspace_id=str(webhook.workspace_id),
|
||||
webhook=str(webhook.id),
|
||||
event_type=str(event),
|
||||
request_method=str(action),
|
||||
request_headers=str(headers),
|
||||
request_body=str(payload),
|
||||
save_webhook_log(
|
||||
webhook=webhook,
|
||||
request_method=action,
|
||||
request_headers=headers,
|
||||
request_body=payload,
|
||||
response_status=500,
|
||||
response_headers="",
|
||||
response_body=str(e),
|
||||
retry_count=str(self.request.retries),
|
||||
retry_count=self.request.retries,
|
||||
event_type=event,
|
||||
)
|
||||
logger.error(f"Webhook {webhook.id} failed with error: {e}")
|
||||
# Retry logic
|
||||
|
||||
@@ -5,9 +5,11 @@ import time
|
||||
import uuid
|
||||
from typing import Dict
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@@ -25,6 +27,13 @@ from plane.db.models import (
|
||||
IssueLabel,
|
||||
IssueSequence,
|
||||
IssueActivity,
|
||||
Page,
|
||||
ProjectPage,
|
||||
Cycle,
|
||||
Module,
|
||||
CycleIssue,
|
||||
ModuleIssue,
|
||||
IssueView,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("plane.worker")
|
||||
@@ -116,13 +125,33 @@ def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]:
|
||||
user_id=workspace_member["member_id"],
|
||||
workspace_id=workspace.id,
|
||||
display_filters={
|
||||
"group_by": None,
|
||||
"order_by": "sort_order",
|
||||
"type": None,
|
||||
"sub_issue": True,
|
||||
"show_empty_groups": True,
|
||||
"layout": "list",
|
||||
"calendar_date_range": "",
|
||||
"calendar": {"layout": "month", "show_weekends": False},
|
||||
"group_by": "state",
|
||||
"order_by": "sort_order",
|
||||
"sub_issue": True,
|
||||
"sub_group_by": None,
|
||||
"show_empty_groups": True,
|
||||
},
|
||||
display_properties={
|
||||
"key": True,
|
||||
"link": True,
|
||||
"cycle": False,
|
||||
"state": True,
|
||||
"labels": False,
|
||||
"modules": False,
|
||||
"assignee": True,
|
||||
"due_date": False,
|
||||
"estimate": True,
|
||||
"priority": True,
|
||||
"created_on": True,
|
||||
"issue_type": True,
|
||||
"start_date": False,
|
||||
"updated_on": True,
|
||||
"customer_count": True,
|
||||
"sub_issue_count": False,
|
||||
"attachment_count": False,
|
||||
"customer_request_count": True,
|
||||
},
|
||||
created_by_id=workspace.created_by_id,
|
||||
)
|
||||
@@ -205,6 +234,8 @@ def create_project_issues(
|
||||
project_map: Dict[int, uuid.UUID],
|
||||
states_map: Dict[int, uuid.UUID],
|
||||
labels_map: Dict[int, uuid.UUID],
|
||||
cycles_map: Dict[int, uuid.UUID],
|
||||
module_map: Dict[int, uuid.UUID],
|
||||
) -> None:
|
||||
"""Creates issues and their associated records for each project.
|
||||
|
||||
@@ -234,6 +265,8 @@ def create_project_issues(
|
||||
labels = issue_seed.pop("labels")
|
||||
project_id = issue_seed.pop("project_id")
|
||||
state_id = issue_seed.pop("state_id")
|
||||
cycle_id = issue_seed.pop("cycle_id")
|
||||
module_ids = issue_seed.pop("module_ids")
|
||||
|
||||
issue = Issue.objects.create(
|
||||
**issue_seed,
|
||||
@@ -259,6 +292,7 @@ def create_project_issues(
|
||||
epoch=time.time(),
|
||||
)
|
||||
|
||||
# Create issue labels
|
||||
for label_id in labels:
|
||||
IssueLabel.objects.create(
|
||||
issue=issue,
|
||||
@@ -268,10 +302,172 @@ def create_project_issues(
|
||||
created_by_id=workspace.created_by_id,
|
||||
)
|
||||
|
||||
# Create cycle issues
|
||||
if cycle_id:
|
||||
CycleIssue.objects.create(
|
||||
issue=issue,
|
||||
cycle_id=cycles_map[cycle_id],
|
||||
project_id=project_map[project_id],
|
||||
workspace_id=workspace.id,
|
||||
created_by_id=workspace.created_by_id,
|
||||
)
|
||||
|
||||
# Create module issues
|
||||
if module_ids:
|
||||
for module_id in module_ids:
|
||||
ModuleIssue.objects.create(
|
||||
issue=issue,
|
||||
module_id=module_map[module_id],
|
||||
project_id=project_map[project_id],
|
||||
workspace_id=workspace.id,
|
||||
created_by_id=workspace.created_by_id,
|
||||
)
|
||||
|
||||
logger.info(f"Task: workspace_seed_task -> Issue {issue_id} created")
|
||||
return
|
||||
|
||||
|
||||
def create_pages(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> None:
|
||||
"""Creates pages for each project in the workspace.
|
||||
|
||||
Args:
|
||||
workspace: The workspace containing the projects
|
||||
project_map: Mapping of seed project IDs to actual project IDs
|
||||
"""
|
||||
page_seeds = read_seed_file("pages.json")
|
||||
|
||||
if not page_seeds:
|
||||
return
|
||||
|
||||
for page_seed in page_seeds:
|
||||
page_id = page_seed.pop("id")
|
||||
|
||||
page = Page.objects.create(
|
||||
workspace_id=workspace.id,
|
||||
is_global=False,
|
||||
access=page_seed.get("access", Page.PUBLIC_ACCESS),
|
||||
name=page_seed.get("name"),
|
||||
description=page_seed.get("description", {}),
|
||||
description_html=page_seed.get("description_html", "<p></p>"),
|
||||
description_binary=page_seed.get("description_binary", None),
|
||||
description_stripped=page_seed.get("description_stripped", None),
|
||||
created_by_id=workspace.created_by_id,
|
||||
updated_by_id=workspace.created_by_id,
|
||||
owned_by_id=workspace.created_by_id,
|
||||
)
|
||||
|
||||
logger.info(f"Task: workspace_seed_task -> Page {page_id} created")
|
||||
if page_seed.get("project_id") and page_seed.get("type") == "PROJECT":
|
||||
ProjectPage.objects.create(
|
||||
workspace_id=workspace.id,
|
||||
project_id=project_map[page_seed.get("project_id")],
|
||||
page_id=page.id,
|
||||
created_by_id=workspace.created_by_id,
|
||||
updated_by_id=workspace.created_by_id,
|
||||
)
|
||||
|
||||
logger.info(f"Task: workspace_seed_task -> Project Page {page_id} created")
|
||||
return
|
||||
|
||||
|
||||
def create_cycles(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> Dict[int, uuid.UUID]:
|
||||
# Create cycles
|
||||
cycle_seeds = read_seed_file("cycles.json")
|
||||
if not cycle_seeds:
|
||||
return
|
||||
|
||||
cycle_map: Dict[int, uuid.UUID] = {}
|
||||
|
||||
for cycle_seed in cycle_seeds:
|
||||
cycle_id = cycle_seed.pop("id")
|
||||
project_id = cycle_seed.pop("project_id")
|
||||
type = cycle_seed.pop("type")
|
||||
|
||||
if type == "CURRENT":
|
||||
start_date = timezone.now()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
if type == "UPCOMING":
|
||||
# Get the last cycle
|
||||
last_cycle = Cycle.objects.filter(project_id=project_map[project_id]).order_by("-end_date").first()
|
||||
if last_cycle:
|
||||
start_date = last_cycle.end_date + timedelta(days=1)
|
||||
end_date = start_date + timedelta(days=14)
|
||||
else:
|
||||
start_date = timezone.now() + timedelta(days=14)
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
cycle = Cycle.objects.create(
|
||||
**cycle_seed,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
project_id=project_map[project_id],
|
||||
workspace=workspace,
|
||||
created_by_id=workspace.created_by_id,
|
||||
owned_by_id=workspace.created_by_id,
|
||||
)
|
||||
|
||||
cycle_map[cycle_id] = cycle.id
|
||||
logger.info(f"Task: workspace_seed_task -> Cycle {cycle_id} created")
|
||||
return cycle_map
|
||||
|
||||
|
||||
def create_modules(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> None:
|
||||
"""Creates modules for each project in the workspace.
|
||||
|
||||
Args:
|
||||
workspace: The workspace containing the projects
|
||||
project_map: Mapping of seed project IDs to actual project IDs
|
||||
"""
|
||||
module_seeds = read_seed_file("modules.json")
|
||||
if not module_seeds:
|
||||
return
|
||||
|
||||
module_map: Dict[int, uuid.UUID] = {}
|
||||
|
||||
for index, module_seed in enumerate(module_seeds):
|
||||
module_id = module_seed.pop("id")
|
||||
project_id = module_seed.pop("project_id")
|
||||
|
||||
start_date = timezone.now() + timedelta(days=index * 2)
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
module = Module.objects.create(
|
||||
**module_seed,
|
||||
start_date=start_date,
|
||||
target_date=end_date,
|
||||
project_id=project_map[project_id],
|
||||
workspace=workspace,
|
||||
created_by_id=workspace.created_by_id,
|
||||
)
|
||||
module_map[module_id] = module.id
|
||||
logger.info(f"Task: workspace_seed_task -> Module {module_id} created")
|
||||
return module_map
|
||||
|
||||
|
||||
def create_views(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> None:
|
||||
"""Creates views for each project in the workspace.
|
||||
|
||||
Args:
|
||||
workspace: The workspace containing the projects
|
||||
project_map: Mapping of seed project IDs to actual project IDs
|
||||
"""
|
||||
|
||||
view_seeds = read_seed_file("views.json")
|
||||
if not view_seeds:
|
||||
return
|
||||
|
||||
for view_seed in view_seeds:
|
||||
project_id = view_seed.pop("project_id")
|
||||
IssueView.objects.create(
|
||||
**view_seed,
|
||||
project_id=project_map[project_id],
|
||||
workspace=workspace,
|
||||
created_by_id=workspace.created_by_id,
|
||||
owned_by_id=workspace.created_by_id,
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def workspace_seed(workspace_id: uuid.UUID) -> None:
|
||||
"""Seeds a new workspace with initial project data.
|
||||
@@ -299,8 +495,20 @@ def workspace_seed(workspace_id: uuid.UUID) -> None:
|
||||
# Create project labels
|
||||
label_map = create_project_labels(workspace, project_map)
|
||||
|
||||
# Create project cycles
|
||||
cycle_map = create_cycles(workspace, project_map)
|
||||
|
||||
# Create project modules
|
||||
module_map = create_modules(workspace, project_map)
|
||||
|
||||
# create project issues
|
||||
create_project_issues(workspace, project_map, state_map, label_map)
|
||||
create_project_issues(workspace, project_map, state_map, label_map, cycle_map, module_map)
|
||||
|
||||
# create project views
|
||||
create_views(workspace, project_map)
|
||||
|
||||
# create project pages
|
||||
create_pages(workspace, project_map)
|
||||
|
||||
logger.info(f"Task: workspace_seed_task -> Workspace {workspace_id} seeded successfully")
|
||||
return
|
||||
|
||||
@@ -31,4 +31,4 @@ class Command(BaseCommand):
|
||||
user.is_active = True
|
||||
user.save()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("User activated succesfully"))
|
||||
self.stdout.write(self.style.SUCCESS("User activated successfully"))
|
||||
|
||||
@@ -59,4 +59,4 @@ class Command(BaseCommand):
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("User password updated succesfully"))
|
||||
self.stdout.write(self.style.SUCCESS("User password updated successfully"))
|
||||
|
||||
@@ -70,7 +70,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
s3_client.delete_object(Bucket=bucket_name, Key="test_permission_check.txt")
|
||||
except ClientError:
|
||||
self.stdout.write("Coudn't delete test object")
|
||||
self.stdout.write("Couldn't delete test object")
|
||||
|
||||
# 4. Test s3:PutBucketPolicy (attempt to put a bucket policy)
|
||||
try:
|
||||
|
||||
@@ -273,6 +273,21 @@ class IssueRelationChoices(models.TextChoices):
|
||||
IMPLEMENTED_BY = "implemented_by", "Implemented By"
|
||||
|
||||
|
||||
# Bidirectional relation pairs: (forward, reverse)
|
||||
# Defined after class to avoid enum metaclass conflicts
|
||||
IssueRelationChoices._RELATION_PAIRS = (
|
||||
("blocked_by", "blocking"),
|
||||
("relates_to", "relates_to"), # symmetric
|
||||
("duplicate", "duplicate"), # symmetric
|
||||
("start_before", "start_after"),
|
||||
("finish_before", "finish_after"),
|
||||
("implemented_by", "implements"),
|
||||
)
|
||||
|
||||
# Generate reverse mapping from pairs
|
||||
IssueRelationChoices._REVERSE_MAPPING = {forward: reverse for forward, reverse in IssueRelationChoices._RELATION_PAIRS}
|
||||
|
||||
|
||||
class IssueRelation(ProjectBaseModel):
|
||||
issue = models.ForeignKey(Issue, related_name="issue_relation", on_delete=models.CASCADE)
|
||||
related_issue = models.ForeignKey(Issue, related_name="issue_related", on_delete=models.CASCADE)
|
||||
|
||||
@@ -12,7 +12,6 @@ from rest_framework.request import Request
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
from plane.db.models import APIActivityLog
|
||||
|
||||
|
||||
api_logger = logging.getLogger("plane.api.request")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from django.core.exceptions import RequestDataTooBig
|
||||
from django.http import JsonResponse
|
||||
|
||||
|
||||
class RequestBodySizeLimitMiddleware:
|
||||
"""
|
||||
Middleware to catch RequestDataTooBig exceptions and return
|
||||
413 Request Entity Too Large instead of 400 Bad Request.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
try:
|
||||
_ = request.body
|
||||
except RequestDataTooBig:
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "REQUEST_BODY_TOO_LARGE",
|
||||
"detail": "The size of the request body exceeds the maximum allowed size.",
|
||||
},
|
||||
status=413,
|
||||
)
|
||||
|
||||
# If body size is OK, continue with the request
|
||||
return self.get_response(request)
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Cycle 1: Getting Started with Plane",
|
||||
"project_id": 1,
|
||||
"sort_order": 1,
|
||||
"timezone": "UTC",
|
||||
"type": "CURRENT"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Cycle 2: Collaboration & Customization",
|
||||
"project_id": 1,
|
||||
"sort_order": 2,
|
||||
"timezone": "UTC",
|
||||
"type": "UPCOMING"
|
||||
}
|
||||
]
|
||||
@@ -6,10 +6,12 @@
|
||||
"description_html": "<p class=\"editor-paragraph-block\">Hey there! This demo project is your playground to get hands-on with Plane. We've set this up so you can click around and see how everything works without worrying about breaking anything.</p><p class=\"editor-paragraph-block\">Each work item is designed to make you familiar with the basics of using Plane. Just follow along card by card at your own pace.</p><p class=\"editor-paragraph-block\">First thing to try</p><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Look in the <strong>Properties</strong> section below where it says <strong>State: Todo</strong>.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click on it and change it to <strong>Done</strong> from the dropdown. Alternatively, you can drag and drop the card to the Done column.</p></li></ol>",
|
||||
"description_stripped": "Hey there! This demo project is your playground to get hands-on with Plane. We've set this up so you can click around and see how everything works without worrying about breaking anything.Each work item is designed to make you familiar with the basics of using Plane. Just follow along card by card at your own pace.First thing to tryLook in the Properties section below where it says State: Todo.Click on it and change it to Done from the dropdown. Alternatively, you can drag and drop the card to the Done column.",
|
||||
"sort_order": 1000,
|
||||
"state_id": 3,
|
||||
"state_id": 4,
|
||||
"labels": [],
|
||||
"priority": "none",
|
||||
"project_id": 1
|
||||
"priority": "urgent",
|
||||
"project_id": 1,
|
||||
"cycle_id": 1,
|
||||
"module_ids": [1]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
@@ -19,8 +21,10 @@
|
||||
"sort_order": 2000,
|
||||
"state_id": 2,
|
||||
"labels": [2],
|
||||
"priority": "none",
|
||||
"project_id": 1
|
||||
"priority": "high",
|
||||
"project_id": 1,
|
||||
"cycle_id": 1,
|
||||
"module_ids": [1]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
@@ -31,8 +35,10 @@
|
||||
"sort_order": 3000,
|
||||
"state_id": 1,
|
||||
"labels": [],
|
||||
"priority": "none",
|
||||
"project_id": 1
|
||||
"priority": "high",
|
||||
"project_id": 1,
|
||||
"cycle_id": 1,
|
||||
"module_ids": [1, 2]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
@@ -41,10 +47,12 @@
|
||||
"description_html": "<p class=\"editor-paragraph-block\">A work item is the fundamental building block of your project. Think of these as the actionable tasks that move your project forward.</p><p class=\"editor-paragraph-block\">Ready to add something to your project's to-do list? Here's how:</p><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click the <strong>Add work item</strong> button in the top-right corner of the Work Items page.</p><image-component src=\"https://media.docs.plane.so/seed_assets/41.png\" width=\"1085.380859375px\" height=\"482.53758375605696px\" id=\"ba055bc3-4162-4750-9ad4-9434fc0e7121\" aspectratio=\"2.249318801089918\"></image-component></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Give your task a clear title and add any details in the description.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Set up the essentials:</p><ul class=\"list-disc pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Assign it to a team member (or yourself!)</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Choose a priority level</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Add start and due dates if there's a timeline</p></li></ul></li></ol><div data-emoji-unicode=\"128161\" data-emoji-url=\"https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png\" data-logo-in-use=\"emoji\" data-background=\"green\" data-block-type=\"callout-component\"><p class=\"editor-paragraph-block\"><strong>Tip:</strong> Save time by using the keyboard shortcut <strong>C</strong> from anywhere in your project to quickly create a new work item!</p></div><div class=\"py-4 border-custom-border-400\" data-type=\"horizontalRule\"><div></div></div><p class=\"editor-paragraph-block\">Want to dive deeper into all the things you can do with work items? Check out our <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" class=\"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer\" href=\"https://docs.plane.so/core-concepts/issues/overview\">documentation</a>.</p>",
|
||||
"description_stripped": "A work item is the fundamental building block of your project. Think of these as the actionable tasks that move your project forward.Ready to add something to your project's to-do list? Here's how:Click the Add work item button in the top-right corner of the Work Items page.Give your task a clear title and add any details in the description.Set up the essentials:Assign it to a team member (or yourself!)Choose a priority levelAdd start and due dates if there's a timelineTip: Save time by using the keyboard shortcut C from anywhere in your project to quickly create a new work item!Want to dive deeper into all the things you can do with work items? Check out our documentation.",
|
||||
"sort_order": 4000,
|
||||
"state_id": 1,
|
||||
"state_id": 3,
|
||||
"labels": [2],
|
||||
"priority": "none",
|
||||
"project_id": 1
|
||||
"priority": "high",
|
||||
"project_id": 1,
|
||||
"cycle_id": 1,
|
||||
"module_ids": [1, 2]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
@@ -53,10 +61,12 @@
|
||||
"description_html": "<p class=\"editor-paragraph-block\">Plane offers multiple ways to look at your work items depending on what you need to see. Let's explore how to change views and customize them!</p><image-component src=\"https://media.docs.plane.so/seed_assets/51.png\" aspectratio=\"4.489130434782608\"></image-component><h2 class=\"editor-heading-block\">Switch between layouts</h2><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Look at the top toolbar in your project. You'll see several layout icons.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click any of these icons to instantly switch between layouts.</p></li></ol><div data-emoji-unicode=\"128161\" data-emoji-url=\"https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png\" data-logo-in-use=\"emoji\" data-background=\"green\" data-block-type=\"callout-component\"><p class=\"editor-paragraph-block\"><strong>Tip:</strong> Different layouts work best for different needs. Try Board view for tracking progress, Calendar for deadline management, and Gantt for timeline planning! See <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" class=\"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer\" href=\"https://docs.plane.so/core-concepts/issues/layouts\"><strong>Layouts</strong></a> for more info.</p></div><h2 class=\"editor-heading-block\">Filter and display options</h2><p class=\"editor-paragraph-block\">Need to focus on specific work?</p><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click the <strong>Filters</strong> dropdown in the toolbar. Select criteria and choose which items to show.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click the <strong>Display</strong> dropdown to tailor how the information appears in your layout</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Created the perfect setup? Save it for later by clicking the the <strong>Save View</strong> button.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Access saved views anytime from the <strong>Views</strong> section in your sidebar.</p></li></ol>",
|
||||
"description_stripped": "Plane offers multiple ways to look at your work items depending on what you need to see. Let's explore how to change views and customize them!Switch between layoutsLook at the top toolbar in your project. You'll see several layout icons.Click any of these icons to instantly switch between layouts.Tip: Different layouts work best for different needs. Try Board view for tracking progress, Calendar for deadline management, and Gantt for timeline planning! See Layouts for more info.Filter and display optionsNeed to focus on specific work?Click the Filters dropdown in the toolbar. Select criteria and choose which items to show.Click the Display dropdown to tailor how the information appears in your layoutCreated the perfect setup? Save it for later by clicking the the Save View button.Access saved views anytime from the Views section in your sidebar.",
|
||||
"sort_order": 5000,
|
||||
"state_id": 1,
|
||||
"state_id": 3,
|
||||
"labels": [],
|
||||
"priority": "none",
|
||||
"project_id": 1
|
||||
"project_id": 1,
|
||||
"cycle_id": 2,
|
||||
"module_ids": [2]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
@@ -67,8 +77,10 @@
|
||||
"sort_order": 6000,
|
||||
"state_id": 1,
|
||||
"labels": [2],
|
||||
"priority": "none",
|
||||
"project_id": 1
|
||||
"priority": "low",
|
||||
"project_id": 1,
|
||||
"cycle_id": 2,
|
||||
"module_ids": [2, 3]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
@@ -80,6 +92,8 @@
|
||||
"state_id": 1,
|
||||
"labels": [],
|
||||
"priority": "none",
|
||||
"project_id": 1
|
||||
"project_id": 1,
|
||||
"cycle_id": 2,
|
||||
"module_ids": [2, 3]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Core Workflow (System)",
|
||||
"project_id": 1,
|
||||
"sort_order": 1,
|
||||
"status": "planned",
|
||||
"description": "Manage, visualize, and track your work items across views."
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Onboarding Flow (Feature)",
|
||||
"project_id": 1,
|
||||
"sort_order": 2,
|
||||
"status": "backlog",
|
||||
"description": "Everything about getting started - creating a project, inviting teammates."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Workspace Setup (Area)",
|
||||
"project_id": 1,
|
||||
"sort_order": 3,
|
||||
"status": "in-progress",
|
||||
"description": "The personalization layer - settings, labels, automations."
|
||||
}
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Project Urgent Tasks",
|
||||
"description": "Project Urgent Tasks",
|
||||
"access": 1,
|
||||
"filters": {},
|
||||
"project_id": 1,
|
||||
"display_filters": {"layout": "list", "calendar": {"layout": "month", "show_weekends": false}, "group_by": "state", "order_by": "sort_order", "sub_issue": false, "sub_group_by": null, "show_empty_groups": false},
|
||||
"display_properties": {"key": true, "link": true, "cycle": true, "state": true, "labels": true, "modules": true, "assignee": true, "due_date": true, "estimate": true, "priority": true, "created_on": true, "issue_type": true, "start_date": true, "updated_on": true, "customer_count": true, "sub_issue_count": true, "attachment_count": true, "customer_request_count": true},
|
||||
"sort_order": 75535,
|
||||
"rich_filters": {"priority__in": "urgent"}
|
||||
}
|
||||
]
|
||||
@@ -62,6 +62,7 @@ MIDDLEWARE = [
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"crum.CurrentRequestUserMiddleware",
|
||||
"django.middleware.gzip.GZipMiddleware",
|
||||
"plane.middleware.request_body_size.RequestBodySizeLimitMiddleware",
|
||||
"plane.middleware.logger.APITokenLogMiddleware",
|
||||
"plane.middleware.logger.RequestLoggerMiddleware",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from django.db import IntegrityError
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from plane.db.models import Cycle, Project, ProjectMember
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project(db, workspace, create_user):
|
||||
"""Create a test project with the user as a member"""
|
||||
project = Project.objects.create(
|
||||
name="Test Project",
|
||||
identifier="TP",
|
||||
workspace=workspace,
|
||||
created_by=create_user,
|
||||
)
|
||||
ProjectMember.objects.create(
|
||||
project=project,
|
||||
member=create_user,
|
||||
role=20, # Admin role
|
||||
is_active=True,
|
||||
)
|
||||
return project
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cycle_data():
|
||||
"""Sample cycle data for tests"""
|
||||
return {
|
||||
"name": "Test Cycle",
|
||||
"description": "A test cycle for unit tests",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def draft_cycle_data():
|
||||
"""Sample draft cycle data (no dates)"""
|
||||
return {
|
||||
"name": "Draft Cycle",
|
||||
"description": "A draft cycle without dates",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_cycle(db, project, create_user):
|
||||
"""Create a test cycle"""
|
||||
return Cycle.objects.create(
|
||||
name="Existing Cycle",
|
||||
description="An existing cycle",
|
||||
start_date=timezone.now() + timedelta(days=1),
|
||||
end_date=timezone.now() + timedelta(days=7),
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
owned_by=create_user,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
class TestCycleListCreateAPIEndpoint:
|
||||
"""Test Cycle List and Create API Endpoint"""
|
||||
|
||||
def get_cycle_url(self, workspace_slug, project_id):
|
||||
"""Helper to get cycle endpoint URL"""
|
||||
return f"/api/v1/workspaces/{workspace_slug}/projects/{project_id}/cycles/"
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_cycle_success(self, api_key_client, workspace, project, cycle_data):
|
||||
"""Test successful cycle creation"""
|
||||
url = self.get_cycle_url(workspace.slug, project.id)
|
||||
|
||||
response = api_key_client.post(url, cycle_data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
assert Cycle.objects.count() == 1
|
||||
|
||||
created_cycle = Cycle.objects.first()
|
||||
assert created_cycle.name == cycle_data["name"]
|
||||
assert created_cycle.description == cycle_data["description"]
|
||||
assert created_cycle.project == project
|
||||
assert created_cycle.owned_by_id is not None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_cycle_invalid_data(self, api_key_client, workspace, project):
|
||||
"""Test cycle creation with invalid data"""
|
||||
url = self.get_cycle_url(workspace.slug, project.id)
|
||||
|
||||
# Test with empty data
|
||||
response = api_key_client.post(url, {}, format="json")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
# Test with missing name
|
||||
response = api_key_client.post(url, {"description": "Test cycle"}, format="json")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_cycle_invalid_date_combination(self, api_key_client, workspace, project):
|
||||
"""Test cycle creation with invalid date combination (only start_date)"""
|
||||
url = self.get_cycle_url(workspace.slug, project.id)
|
||||
|
||||
invalid_data = {
|
||||
"name": "Invalid Cycle",
|
||||
"start_date": (timezone.now() + timedelta(days=1)).isoformat(),
|
||||
# Missing end_date
|
||||
}
|
||||
|
||||
response = api_key_client.post(url, invalid_data, format="json")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "Both start date and end date are either required or are to be null" in response.data["error"]
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_cycle_with_external_id(self, api_key_client, workspace, project):
|
||||
"""Test creating cycle with external ID"""
|
||||
url = self.get_cycle_url(workspace.slug, project.id)
|
||||
|
||||
cycle_data = {
|
||||
"name": "External Cycle",
|
||||
"description": "A cycle with external ID",
|
||||
"external_id": "ext-123",
|
||||
"external_source": "github",
|
||||
}
|
||||
|
||||
response = api_key_client.post(url, cycle_data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
created_cycle = Cycle.objects.first()
|
||||
assert created_cycle.external_id == "ext-123"
|
||||
assert created_cycle.external_source == "github"
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_cycle_duplicate_external_id(self, api_key_client, workspace, project, create_user):
|
||||
"""Test creating cycle with duplicate external ID"""
|
||||
url = self.get_cycle_url(workspace.slug, project.id)
|
||||
|
||||
# Create first cycle
|
||||
Cycle.objects.create(
|
||||
name="First Cycle",
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
external_id="ext-123",
|
||||
external_source="github",
|
||||
owned_by=create_user,
|
||||
)
|
||||
|
||||
# Try to create second cycle with same external ID
|
||||
cycle_data = {
|
||||
"name": "Second Cycle",
|
||||
"external_id": "ext-123",
|
||||
"external_source": "github",
|
||||
"owned_by": create_user.id,
|
||||
}
|
||||
|
||||
response = api_key_client.post(url, cycle_data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
assert "same external id" in response.data["error"]
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_cycles_success(self, api_key_client, workspace, project, create_cycle, create_user):
|
||||
"""Test successful cycle listing"""
|
||||
url = self.get_cycle_url(workspace.slug, project.id)
|
||||
|
||||
# Create additional cycles
|
||||
Cycle.objects.create(
|
||||
name="Cycle 2",
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
start_date=timezone.now() + timedelta(days=10),
|
||||
end_date=timezone.now() + timedelta(days=17),
|
||||
owned_by=create_user,
|
||||
)
|
||||
Cycle.objects.create(
|
||||
name="Cycle 3",
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
start_date=timezone.now() + timedelta(days=20),
|
||||
end_date=timezone.now() + timedelta(days=27),
|
||||
owned_by=create_user,
|
||||
)
|
||||
|
||||
response = api_key_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert "results" in response.data
|
||||
assert len(response.data["results"]) == 3 # Including create_cycle fixture
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_cycles_with_view_filter(self, api_key_client, workspace, project, create_user):
|
||||
"""Test cycle listing with different view filters"""
|
||||
url = self.get_cycle_url(workspace.slug, project.id)
|
||||
|
||||
# Create cycles in different states
|
||||
now = timezone.now()
|
||||
|
||||
# Current cycle (started but not ended)
|
||||
Cycle.objects.create(
|
||||
name="Current Cycle",
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
start_date=now - timedelta(days=1),
|
||||
end_date=now + timedelta(days=6),
|
||||
owned_by=create_user,
|
||||
)
|
||||
|
||||
# Upcoming cycle
|
||||
Cycle.objects.create(
|
||||
name="Upcoming Cycle",
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
start_date=now + timedelta(days=1),
|
||||
end_date=now + timedelta(days=8),
|
||||
owned_by=create_user,
|
||||
)
|
||||
|
||||
# Completed cycle
|
||||
Cycle.objects.create(
|
||||
name="Completed Cycle",
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
start_date=now - timedelta(days=10),
|
||||
end_date=now - timedelta(days=3),
|
||||
owned_by=create_user,
|
||||
)
|
||||
|
||||
# Draft cycle
|
||||
Cycle.objects.create(
|
||||
name="Draft Cycle",
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
owned_by=create_user,
|
||||
)
|
||||
|
||||
# Test current cycles
|
||||
response = api_key_client.get(url, {"cycle_view": "current"})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data) == 1
|
||||
assert response.data[0]["name"] == "Current Cycle"
|
||||
|
||||
# Test upcoming cycles
|
||||
response = api_key_client.get(url, {"cycle_view": "upcoming"})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data["results"]) == 1
|
||||
assert response.data["results"][0]["name"] == "Upcoming Cycle"
|
||||
|
||||
# Test completed cycles
|
||||
response = api_key_client.get(url, {"cycle_view": "completed"})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data["results"]) == 1
|
||||
assert response.data["results"][0]["name"] == "Completed Cycle"
|
||||
|
||||
# Test draft cycles
|
||||
response = api_key_client.get(url, {"cycle_view": "draft"})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data["results"]) == 1
|
||||
assert response.data["results"][0]["name"] == "Draft Cycle"
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
class TestCycleDetailAPIEndpoint:
|
||||
"""Test Cycle Detail API Endpoint"""
|
||||
|
||||
def get_cycle_detail_url(self, workspace_slug, project_id, cycle_id):
|
||||
"""Helper to get cycle detail endpoint URL"""
|
||||
return f"/api/v1/workspaces/{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/"
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_cycle_success(self, api_key_client, workspace, project, create_cycle):
|
||||
"""Test successful cycle retrieval"""
|
||||
url = self.get_cycle_detail_url(workspace.slug, project.id, create_cycle.id)
|
||||
|
||||
response = api_key_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert str(response.data["id"]) == str(create_cycle.id)
|
||||
assert response.data["name"] == create_cycle.name
|
||||
assert response.data["description"] == create_cycle.description
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_cycle_not_found(self, api_key_client, workspace, project):
|
||||
"""Test getting non-existent cycle"""
|
||||
fake_id = uuid4()
|
||||
url = self.get_cycle_detail_url(workspace.slug, project.id, fake_id)
|
||||
|
||||
response = api_key_client.get(url)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_cycle_success(self, api_key_client, workspace, project, create_cycle):
|
||||
"""Test successful cycle update"""
|
||||
url = self.get_cycle_detail_url(workspace.slug, project.id, create_cycle.id)
|
||||
|
||||
update_data = {
|
||||
"name": f"Updated Cycle {uuid4()}",
|
||||
"description": "Updated description",
|
||||
}
|
||||
|
||||
response = api_key_client.patch(url, update_data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
create_cycle.refresh_from_db()
|
||||
assert create_cycle.name == update_data["name"]
|
||||
assert create_cycle.description == update_data["description"]
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_cycle_invalid_data(self, api_key_client, workspace, project, create_cycle):
|
||||
"""Test cycle update with invalid data"""
|
||||
url = self.get_cycle_detail_url(workspace.slug, project.id, create_cycle.id)
|
||||
|
||||
update_data = {"name": ""}
|
||||
response = api_key_client.patch(url, update_data, format="json")
|
||||
|
||||
# This might be 400 if name is required, or 200 if empty names are allowed
|
||||
assert response.status_code in [status.HTTP_400_BAD_REQUEST, status.HTTP_200_OK]
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_cycle_with_external_id_conflict(self, api_key_client, workspace, project, create_cycle, create_user ):
|
||||
"""Test cycle update with conflicting external ID"""
|
||||
url = self.get_cycle_detail_url(workspace.slug, project.id, create_cycle.id)
|
||||
|
||||
# Create another cycle with external ID
|
||||
Cycle.objects.create(
|
||||
name="Another Cycle",
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
external_id="ext-456",
|
||||
external_source="github",
|
||||
owned_by=create_user,
|
||||
)
|
||||
|
||||
# Try to update cycle with same external ID
|
||||
update_data = {
|
||||
"external_id": "ext-456",
|
||||
"external_source": "github",
|
||||
}
|
||||
|
||||
response = api_key_client.patch(url, update_data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
assert "same external id" in response.data["error"]
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_cycle_success(self, api_key_client, workspace, project, create_cycle):
|
||||
"""Test successful cycle deletion"""
|
||||
url = self.get_cycle_detail_url(workspace.slug, project.id, create_cycle.id)
|
||||
|
||||
response = api_key_client.delete(url)
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert not Cycle.objects.filter(id=create_cycle.id).exists()
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cycle_metrics_annotation(self, api_key_client, workspace, project, create_cycle):
|
||||
"""Test that cycle includes issue metrics annotations"""
|
||||
url = self.get_cycle_detail_url(workspace.slug, project.id, create_cycle.id)
|
||||
|
||||
response = api_key_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Check that metrics are included in response
|
||||
cycle_data = response.data
|
||||
assert "total_issues" in cycle_data
|
||||
assert "completed_issues" in cycle_data
|
||||
assert "cancelled_issues" in cycle_data
|
||||
assert "started_issues" in cycle_data
|
||||
assert "unstarted_issues" in cycle_data
|
||||
assert "backlog_issues" in cycle_data
|
||||
|
||||
# All should be 0 for a new cycle
|
||||
assert cycle_data["total_issues"] == 0
|
||||
assert cycle_data["completed_issues"] == 0
|
||||
assert cycle_data["cancelled_issues"] == 0
|
||||
assert cycle_data["started_issues"] == 0
|
||||
assert cycle_data["unstarted_issues"] == 0
|
||||
assert cycle_data["backlog_issues"] == 0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user