Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
707570ca7a | ||
|
|
156f1011f3 | ||
|
|
a36d1a753e | ||
|
|
9a927ded84 | ||
|
|
72f00e378d | ||
|
|
d3ec1aa422 | ||
|
|
dda19b0a3f | ||
|
|
2b570da890 | ||
|
|
5918607171 | ||
|
|
f1496e3144 | ||
|
|
9717497b4e | ||
|
|
2f8c8ac40f | ||
|
|
734e920e08 | ||
|
|
82fa1347d1 | ||
|
|
a7aa5c2ba7 | ||
|
|
825b2f26bf | ||
|
|
af51992eba | ||
|
|
8f59a36bda | ||
|
|
5ad0114aac | ||
|
|
095639b976 | ||
|
|
db722d580f | ||
|
|
0363057d9c | ||
|
|
3dc933f0e8 | ||
|
|
fc33238d89 | ||
|
|
86464c1d6f | ||
|
|
cc479f39a7 | ||
|
|
b50df9ef99 | ||
|
|
c637639a3e | ||
|
|
12401c54cc | ||
|
|
1201a4245e | ||
|
|
fc15ca5565 | ||
|
|
4e8b7e6dbb | ||
|
|
b0bc818362 | ||
|
|
c8491a13b3 | ||
|
|
83587c2c6b | ||
|
|
d9d62c2d5a | ||
|
|
b591203da6 | ||
|
|
78e0405971 | ||
|
|
26040144fc | ||
|
|
0fd36257d7 | ||
|
|
c217185b07 | ||
|
|
a5628c4ce1 | ||
|
|
764e08140c | ||
|
|
c76af7d7d6 | ||
|
|
1dcea9bcc8 | ||
|
|
da957e06b6 | ||
|
|
a0b9596cb4 | ||
|
|
f71e8a3a0f | ||
|
|
002fb4547b | ||
|
|
c1b1ba35c1 | ||
|
|
4566d6e80c | ||
|
|
e8d359e625 | ||
|
|
351eba8d61 | ||
|
|
1e27e37b51 | ||
|
|
7df2e9cf11 | ||
|
|
c6e3f1b932 |
@@ -2,6 +2,11 @@ name: Build AIO Base Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_tag_name:
|
||||
description: 'Base Tag Name'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
@@ -22,27 +27,29 @@ jobs:
|
||||
- id: set_env_variables
|
||||
name: Set Environment Variables
|
||||
run: |
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
|
||||
echo "IMAGE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
echo "IMAGE_TAG=latest" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
echo "IMAGE_TAG=preview" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "IMAGE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=makeplane/plane-dev" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "IMAGE_TAG=latest" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
echo "TARGET_BRANCH=preview" >> $GITHUB_OUTPUT
|
||||
echo "IMAGE_TAG=preview" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "TARGET_BRANCH=develop" >> $GITHUB_OUTPUT
|
||||
echo "IMAGE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
- id: checkout_files
|
||||
@@ -54,7 +61,6 @@ jobs:
|
||||
needs: [base_build_setup]
|
||||
env:
|
||||
BASE_IMG_TAG: makeplane/plane-aio-base:full-${{ needs.base_build_setup.outputs.image_tag }}
|
||||
TARGET_BRANCH: ${{ needs.base_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.base_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.base_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.base_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -96,7 +102,6 @@ jobs:
|
||||
needs: [base_build_setup]
|
||||
env:
|
||||
BASE_IMG_TAG: makeplane/plane-aio-base:slim-${{ needs.base_build_setup.outputs.image_tag }}
|
||||
TARGET_BRANCH: ${{ needs.base_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.base_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.base_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.base_build_setup.outputs.gh_buildx_platforms }}
|
||||
|
||||
@@ -13,6 +13,10 @@ on:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
base_tag_name:
|
||||
description: 'Base Tag Name'
|
||||
required: false
|
||||
default: ''
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
@@ -52,7 +56,9 @@ jobs:
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
if [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
|
||||
echo "AIO_BASE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
echo "AIO_BASE_TAG=preview" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -2,6 +2,11 @@ name: Feature Preview
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_tag_name:
|
||||
description: 'Base Tag Name'
|
||||
required: false
|
||||
default: 'preview'
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
@@ -29,7 +34,12 @@ jobs:
|
||||
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
|
||||
echo "AIO_BASE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigura
|
||||
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
CodeBlock,
|
||||
ConfirmDiscardModal,
|
||||
ControllerInput,
|
||||
CopyField,
|
||||
@@ -102,7 +103,8 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
||||
url: originURL,
|
||||
description: (
|
||||
<>
|
||||
We will auto-generate this. Paste this into the Authorized origin URL field{" "}
|
||||
We will auto-generate this. Paste this into the{" "}
|
||||
<CodeBlock darkerShade>Authorized origin URL</CodeBlock> field{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
@@ -121,7 +123,8 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
||||
url: `${originURL}/auth/github/callback/`,
|
||||
description: (
|
||||
<>
|
||||
We will auto-generate this. Paste this into your Authorized Callback URI field{" "}
|
||||
We will auto-generate this. Paste this into your{" "}
|
||||
<CodeBlock darkerShade>Authorized Callback URI</CodeBlock> field{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
@@ -143,8 +146,8 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
||||
.then((response = []) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
message: "Github Configuration Settings updated successfully",
|
||||
title: "Done!",
|
||||
message: "Your GitHub authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
|
||||
@@ -170,8 +173,8 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
||||
/>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1">
|
||||
<div className="pt-2 text-xl font-medium">Configuration</div>
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
||||
<div className="pt-2.5 text-xl font-medium">GitHub-provided details for Plane</div>
|
||||
{GITHUB_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -201,8 +204,8 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Service provider details</div>
|
||||
<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} />
|
||||
))}
|
||||
|
||||
@@ -93,7 +93,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceGithubConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
|
||||
@@ -8,6 +8,7 @@ import { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigura
|
||||
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
CodeBlock,
|
||||
ConfirmDiscardModal,
|
||||
ControllerInput,
|
||||
CopyField,
|
||||
@@ -54,7 +55,7 @@ export const InstanceGitlabConfigForm: FC<Props> = (props) => {
|
||||
label: "Host",
|
||||
description: (
|
||||
<>
|
||||
This is the <b>GitLab host</b> to use for login, <b>including scheme</b>.
|
||||
This is either https://gitlab.com or the <CodeBlock>domain.tld</CodeBlock> where you host GitLab.
|
||||
</>
|
||||
),
|
||||
placeholder: "https://gitlab.com",
|
||||
@@ -116,7 +117,8 @@ export const InstanceGitlabConfigForm: FC<Props> = (props) => {
|
||||
url: `${originURL}/auth/gitlab/callback/`,
|
||||
description: (
|
||||
<>
|
||||
We will auto-generate this. Paste this into the <b>Redirect URI</b> field of your{" "}
|
||||
We will auto-generate this. Paste this into the{" "}
|
||||
<CodeBlock darkerShade>Redirect URI</CodeBlock> field of your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
@@ -139,8 +141,8 @@ export const InstanceGitlabConfigForm: FC<Props> = (props) => {
|
||||
.then((response = []) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
message: "GitLab Configuration Settings updated successfully",
|
||||
title: "Done!",
|
||||
message: "Your GitLab authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
|
||||
@@ -167,8 +169,8 @@ export const InstanceGitlabConfigForm: FC<Props> = (props) => {
|
||||
/>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1">
|
||||
<div className="pt-2 text-xl font-medium">Configuration</div>
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
||||
<div className="pt-2.5 text-xl font-medium">GitLab-provided details for Plane</div>
|
||||
{GITLAB_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -198,8 +200,8 @@ export const InstanceGitlabConfigForm: FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Service provider details</div>
|
||||
<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 GitLab</div>
|
||||
{GITLAB_SERVICE_FIELD.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
|
||||
@@ -80,7 +80,7 @@ const InstanceGitlabAuthenticationPage = observer(() => {
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceGitlabConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
|
||||
@@ -9,6 +9,7 @@ import { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigura
|
||||
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
CodeBlock,
|
||||
ConfirmDiscardModal,
|
||||
ControllerInput,
|
||||
CopyField,
|
||||
@@ -101,7 +102,8 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
||||
url: originURL,
|
||||
description: (
|
||||
<p>
|
||||
We will auto-generate this. Paste this into your Authorized JavaScript origins field. For this OAuth client{" "}
|
||||
We will auto-generate this. Paste this into your{" "}
|
||||
<CodeBlock darkerShade>Authorized JavaScript origins</CodeBlock> field. For this OAuth client{" "}
|
||||
<a
|
||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||
target="_blank"
|
||||
@@ -119,7 +121,8 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
||||
url: `${originURL}/auth/google/callback/`,
|
||||
description: (
|
||||
<p>
|
||||
We will auto-generate this. Paste this into your Authorized Redirect URI field. For this OAuth client{" "}
|
||||
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Redirect URI</CodeBlock>{" "}
|
||||
field. For this OAuth client{" "}
|
||||
<a
|
||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||
target="_blank"
|
||||
@@ -140,8 +143,8 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
||||
.then((response = []) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
message: "Google Configuration Settings updated successfully",
|
||||
title: "Done!",
|
||||
message: "Your Google authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
|
||||
@@ -167,8 +170,8 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
||||
/>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1">
|
||||
<div className="pt-2 text-xl font-medium">Configuration</div>
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
||||
<div className="pt-2.5 text-xl font-medium">Google-provided details for Plane</div>
|
||||
{GOOGLE_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -198,8 +201,8 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Service provider details</div>
|
||||
<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} />
|
||||
))}
|
||||
|
||||
@@ -81,7 +81,7 @@ const InstanceGoogleAuthenticationPage = observer(() => {
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
|
||||
@@ -1,84 +1,48 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { KeyRound, Mails } from "lucide-react";
|
||||
// types
|
||||
import { TInstanceAuthenticationMethodKeys, TInstanceAuthenticationModes } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
AuthenticationMethodCard,
|
||||
EmailCodesConfiguration,
|
||||
GithubConfiguration,
|
||||
GitlabConfiguration,
|
||||
GoogleConfiguration,
|
||||
PasswordLoginConfiguration,
|
||||
} from "@/components/authentication";
|
||||
TGetBaseAuthenticationModeProps,
|
||||
TInstanceAuthenticationMethodKeys,
|
||||
TInstanceAuthenticationModes,
|
||||
} from "@plane/types";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication";
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "@/helpers/common.helper";
|
||||
import { UpgradeButton } from "@/components/common/upgrade-button";
|
||||
import { getBaseAuthenticationModes } from "@/helpers/authentication.helper";
|
||||
// images
|
||||
import githubLightModeImage from "@/public/logos/github-black.png";
|
||||
import githubDarkModeImage from "@/public/logos/github-white.png";
|
||||
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
|
||||
import GoogleLogo from "@/public/logos/google-logo.svg";
|
||||
import OIDCLogo from "@/public/logos/oidc-logo.svg";
|
||||
import SAMLLogo from "@/public/logos/saml-logo.svg";
|
||||
|
||||
export type TAuthenticationModeProps = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export type TGetAuthenticationModeProps = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
resolvedTheme: string | undefined;
|
||||
};
|
||||
|
||||
// Authentication methods
|
||||
export const getAuthenticationModes: (props: TGetAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
|
||||
export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
|
||||
disabled,
|
||||
updateConfig,
|
||||
resolvedTheme,
|
||||
}) => [
|
||||
...getBaseAuthenticationModes({ disabled, updateConfig, resolvedTheme }),
|
||||
{
|
||||
key: "unique-codes",
|
||||
name: "Unique codes",
|
||||
description: "Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
|
||||
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
||||
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
key: "oidc",
|
||||
name: "OIDC",
|
||||
description: "Authenticate your users via the OpenID Connect protocol.",
|
||||
icon: <Image src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
{
|
||||
key: "passwords-login",
|
||||
name: "Passwords",
|
||||
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
|
||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
||||
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "google",
|
||||
name: "Google",
|
||||
description: "Allow members to log in or sign up for Plane with their Google accounts.",
|
||||
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
|
||||
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "github",
|
||||
name: "GitHub",
|
||||
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
|
||||
icon: (
|
||||
<Image
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
height={20}
|
||||
width={20}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
),
|
||||
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "gitlab",
|
||||
name: "GitLab",
|
||||
description: "Allow members to login or sign up to plane with their GitLab accounts.",
|
||||
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
||||
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
key: "saml",
|
||||
name: "SAML",
|
||||
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
|
||||
icon: <Image src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -97,6 +61,7 @@ export const AuthenticationModes: React.FC<TAuthenticationModeProps> = observer(
|
||||
icon={method.icon}
|
||||
config={method.config}
|
||||
disabled={disabled}
|
||||
unavailable={method.unavailable}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -41,10 +41,10 @@ export const InstanceSidebar: FC<IInstanceSidebar> = observer(() => {
|
||||
<div
|
||||
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
|
||||
fixed md:relative
|
||||
${isSidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
sm:${isSidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[280px]"}
|
||||
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[280px]"}
|
||||
${isSidebarCollapsed ? "-ml-[290px]" : ""}
|
||||
sm:${isSidebarCollapsed ? "-ml-[290px]" : ""}
|
||||
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"}
|
||||
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"}
|
||||
`}
|
||||
>
|
||||
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
|
||||
|
||||
@@ -11,10 +11,11 @@ type Props = {
|
||||
config: JSX.Element;
|
||||
disabled?: boolean;
|
||||
withBorder?: boolean;
|
||||
unavailable?: boolean;
|
||||
};
|
||||
|
||||
export const AuthenticationMethodCard: FC<Props> = (props) => {
|
||||
const { name, description, icon, config, disabled = false, withBorder = true } = props;
|
||||
const { name, description, icon, config, disabled = false, withBorder = true, unavailable = false } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -22,7 +23,11 @@ export const AuthenticationMethodCard: FC<Props> = (props) => {
|
||||
"px-4 py-3 border border-custom-border-200": withBorder,
|
||||
})}
|
||||
>
|
||||
<div className="flex grow items-center gap-4">
|
||||
<div
|
||||
className={cn("flex grow items-center gap-4", {
|
||||
"opacity-50": unavailable,
|
||||
})}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-custom-background-80">{icon}</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type TProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
darkerShade?: boolean;
|
||||
};
|
||||
|
||||
export const CodeBlock = ({ children, className, darkerShade }: TProps) => (
|
||||
<span
|
||||
className={cn(
|
||||
"px-0.5 text-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
|
||||
{
|
||||
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
@@ -38,7 +38,7 @@ export const ControllerInput: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">
|
||||
{label} {!required && "(optional)"}
|
||||
{label}
|
||||
</h4>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
@@ -80,7 +80,7 @@ export const ControllerInput: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{description && <p className="text-xs text-custom-text-300">{description}</p>}
|
||||
{description && <p className="pt-0.5 text-xs text-custom-text-300">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,3 +7,5 @@ export * from "./banner";
|
||||
export * from "./empty-state";
|
||||
export * from "./logo-spinner";
|
||||
export * from "./page-header";
|
||||
export * from "./code-block";
|
||||
export * from "./upgrade-button";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
// icons
|
||||
import { SquareArrowOutUpRight } from "lucide-react";
|
||||
// ui
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
export const UpgradeButton: React.FC = () => (
|
||||
<a href="https://plane.so/one" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
|
||||
Available on One
|
||||
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
|
||||
</a>
|
||||
);
|
||||
@@ -1,7 +1,24 @@
|
||||
import { ReactNode } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { KeyRound, Mails } from "lucide-react";
|
||||
// types
|
||||
import { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
EmailCodesConfiguration,
|
||||
GithubConfiguration,
|
||||
GitlabConfiguration,
|
||||
GoogleConfiguration,
|
||||
PasswordLoginConfiguration,
|
||||
} from "@/components/authentication";
|
||||
// helpers
|
||||
import { SUPPORT_EMAIL } from "./common.helper";
|
||||
import { SUPPORT_EMAIL, resolveGeneralTheme } from "@/helpers/common.helper";
|
||||
// images
|
||||
import githubLightModeImage from "@/public/logos/github-black.png";
|
||||
import githubDarkModeImage from "@/public/logos/github-white.png";
|
||||
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
|
||||
import GoogleLogo from "@/public/logos/google-logo.svg";
|
||||
|
||||
export enum EPageTypes {
|
||||
PUBLIC = "PUBLIC",
|
||||
@@ -134,3 +151,53 @@ export const authErrorHandler = (
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
|
||||
disabled,
|
||||
updateConfig,
|
||||
resolvedTheme,
|
||||
}) => [
|
||||
{
|
||||
key: "unique-codes",
|
||||
name: "Unique codes",
|
||||
description:
|
||||
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
|
||||
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
||||
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "passwords-login",
|
||||
name: "Passwords",
|
||||
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
|
||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
||||
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "google",
|
||||
name: "Google",
|
||||
description: "Allow members to log in or sign up for Plane with their Google accounts.",
|
||||
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
|
||||
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "github",
|
||||
name: "GitHub",
|
||||
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
|
||||
icon: (
|
||||
<Image
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
height={20}
|
||||
width={20}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
),
|
||||
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "gitlab",
|
||||
name: "GitLab",
|
||||
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
|
||||
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
||||
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
];
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg width="92" height="84" viewBox="0 0 92 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3695_11896)">
|
||||
<path d="M83.0398 32.6876C74.2901 27.2397 62.0735 23.8553 48.7013 23.8553C21.7918 23.8553 0 37.3101 0 53.9016C0 69.0898 18.1598 81.554 41.685 83.7001V74.9504C25.8364 72.9693 13.95 64.3022 13.95 53.9016C13.95 42.0977 29.4684 32.44 48.7013 32.44C58.2765 32.44 66.9436 34.8338 73.217 38.7134L64.3022 44.2439H92.1197V27.0746L83.0398 32.6876Z" fill="#CCCCCC"/>
|
||||
<path d="M41.6846 8.99736V74.9504V83.7002L55.6346 74.9504V0L41.6846 8.99736Z" fill="#FF6200"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3695_11896">
|
||||
<rect width="92" height="84" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 702 B |
@@ -0,0 +1,17 @@
|
||||
<svg width="700" height="650" viewBox="0 0 700 650" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3262_5767)">
|
||||
<mask id="mask0_3262_5767" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="700" height="650">
|
||||
<path d="M700 0H0V650H700V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3262_5767)">
|
||||
<path d="M337.682 0L360.832 20.5C377.982 35.7 395.182 50.85 412.132 66.25C521.982 166 614.982 278.25 684.982 407.45C688.582 414.05 691.832 420.85 695.082 427.6L699.982 437.75L694.582 440.6L690.532 434.85L680.032 419.9L672.682 409.2C621.732 335.25 570.682 261.2 500.582 201.95C479.373 183.995 455.969 168.807 430.932 156.75C380.232 132.5 335.132 142.2 296.432 182C259.632 219.85 240.532 266.85 223.282 314.65C221.032 320.8 218.682 326.9 216.332 333L212.232 343.75L203.632 341C208.632 323.6 213.232 306.1 217.832 288.55C228.332 248.8 238.832 209.05 253.432 170.75C268.932 129.95 288.532 90.6 308.082 51.25C316.532 34.2 324.982 17.15 333.082 0H337.682Z" fill="#C22E33"/>
|
||||
<path d="M372.382 491.1C291.082 529.6 94.3829 569.3 1.08287 559.1C-14.1671 478.8 135.482 102.5 208.982 45.5L204.232 56.4C202.115 61.531 199.813 66.5842 197.332 71.55L194.032 78C156.032 151.1 118.082 224.3 98.6329 304.5C91.6287 332.124 87.8038 360.458 87.2328 388.95C86.7328 455.95 128.432 501.55 198.082 504.4C231.582 505.75 265.432 502.25 299.232 498.7C313.932 497.2 328.582 495.65 343.232 494.5C348.632 494.1 353.932 493.45 360.832 492.55L372.382 491.15V491.1Z" fill="#C22E33"/>
|
||||
<path d="M141.233 639.05C118.983 640.75 96.733 642.45 74.583 644.45C279.433 663.95 476.083 630.6 670.083 562.25C606.833 450.75 521.583 362.7 422.483 286.15C423.783 291.05 426.683 294.6 429.533 298.1L431.933 301.1C440.433 312.4 449.333 323.5 458.283 334.6C478.733 360.05 499.183 385.5 514.583 413.5C553.483 484.5 532.383 545.9 456.183 578.3C406.083 599.65 351.333 614.2 297.183 622.9C245.683 631.1 193.433 635.05 141.233 639.05Z" fill="#C22E33"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3262_5767">
|
||||
<rect width="700" height="650" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.21.0"
|
||||
"version": "0.22.0"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.db.models import (
|
||||
OuterRef,
|
||||
Q,
|
||||
Sum,
|
||||
IntegerField,
|
||||
FloatField,
|
||||
)
|
||||
from django.db.models.functions import Cast
|
||||
|
||||
@@ -867,12 +867,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -882,7 +882,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -921,12 +921,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -936,7 +936,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
|
||||
@@ -149,7 +149,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
description_html=request.data.get("issue", {}).get(
|
||||
"description_html", "<p></p>"
|
||||
),
|
||||
priority=request.data.get("issue", {}).get("priority", "low"),
|
||||
priority=request.data.get("issue", {}).get("priority", "none"),
|
||||
project_id=project_id,
|
||||
state=state,
|
||||
)
|
||||
|
||||
@@ -177,8 +177,8 @@ class ModuleSerializer(DynamicBaseSerializer):
|
||||
started_issues = serializers.IntegerField(read_only=True)
|
||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||
backlog_issues = serializers.IntegerField(read_only=True)
|
||||
total_estimate_points = serializers.IntegerField(read_only=True)
|
||||
completed_estimate_points = serializers.IntegerField(read_only=True)
|
||||
total_estimate_points = serializers.FloatField(read_only=True)
|
||||
completed_estimate_points = serializers.FloatField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
@@ -222,10 +222,10 @@ class ModuleSerializer(DynamicBaseSerializer):
|
||||
class ModuleDetailSerializer(ModuleSerializer):
|
||||
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
||||
sub_issues = serializers.IntegerField(read_only=True)
|
||||
backlog_estimate_points = serializers.IntegerField(read_only=True)
|
||||
unstarted_estimate_points = serializers.IntegerField(read_only=True)
|
||||
started_estimate_points = serializers.IntegerField(read_only=True)
|
||||
cancelled_estimate_points = serializers.IntegerField(read_only=True)
|
||||
backlog_estimate_points = serializers.FloatField(read_only=True)
|
||||
unstarted_estimate_points = serializers.FloatField(read_only=True)
|
||||
started_estimate_points = serializers.FloatField(read_only=True)
|
||||
cancelled_estimate_points = serializers.FloatField(read_only=True)
|
||||
|
||||
class Meta(ModuleSerializer.Meta):
|
||||
fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues", "backlog_estimate_points", "unstarted_estimate_points", "started_estimate_points", "cancelled_estimate_points"]
|
||||
|
||||
@@ -3,11 +3,15 @@ from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from plane.db.models import Notification, UserNotificationPreference
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class NotificationSerializer(BaseSerializer):
|
||||
triggered_by_details = UserLiteSerializer(
|
||||
read_only=True, source="triggered_by"
|
||||
)
|
||||
is_inbox_issue = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Notification
|
||||
|
||||
@@ -15,6 +15,7 @@ from plane.db.models import (
|
||||
WorkspaceTheme,
|
||||
WorkspaceUserProperties,
|
||||
)
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
@@ -22,22 +23,11 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validated(self, data):
|
||||
if data.get("slug") in [
|
||||
"404",
|
||||
"accounts",
|
||||
"api",
|
||||
"create-workspace",
|
||||
"god-mode",
|
||||
"installations",
|
||||
"invitations",
|
||||
"onboarding",
|
||||
"profile",
|
||||
"spaces",
|
||||
"workspace-invitations",
|
||||
"password",
|
||||
]:
|
||||
raise serializers.ValidationError({"slug": "Slug is not valid"})
|
||||
def validate_slug(self, value):
|
||||
# Check if the slug is restricted
|
||||
if value in RESTRICTED_WORKSPACE_SLUGS:
|
||||
raise serializers.ValidationError("Slug is not valid")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = Workspace
|
||||
|
||||
@@ -19,7 +19,7 @@ from django.db.models import (
|
||||
When,
|
||||
Subquery,
|
||||
Sum,
|
||||
IntegerField,
|
||||
FloatField,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.utils import timezone
|
||||
@@ -86,7 +86,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
backlog_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("backlog_estimate_point")[:1]
|
||||
@@ -100,7 +100,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
unstarted_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("unstarted_estimate_point")[:1]
|
||||
@@ -114,7 +114,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
started_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("started_estimate_point")[:1]
|
||||
@@ -128,7 +128,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
cancelled_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("cancelled_estimate_point")[:1]
|
||||
@@ -142,7 +142,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("completed_estimate_points")[:1]
|
||||
@@ -155,7 +155,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("total_estimate_points")[:1]
|
||||
@@ -287,37 +287,37 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(
|
||||
backlog_estimate_points=Coalesce(
|
||||
Subquery(backlog_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
unstarted_estimate_points=Coalesce(
|
||||
Subquery(unstarted_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
started_estimate_points=Coalesce(
|
||||
Subquery(started_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
cancelled_estimate_points=Coalesce(
|
||||
Subquery(cancelled_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimate_points=Coalesce(
|
||||
Subquery(completed_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.order_by("-is_favorite", "name")
|
||||
@@ -395,12 +395,12 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -410,7 +410,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -433,12 +433,12 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -448,7 +448,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -836,12 +836,12 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -851,7 +851,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -874,12 +874,12 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -889,7 +889,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -1234,12 +1234,12 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -1249,7 +1249,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -1288,12 +1288,12 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -1303,7 +1303,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
|
||||
@@ -99,6 +99,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
),
|
||||
)
|
||||
.annotate(state_group=F("state__group"))
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
# create's a dict with state group name with their respective issue id's
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.db.models import (
|
||||
UUIDField,
|
||||
Value,
|
||||
Sum,
|
||||
FloatField,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
@@ -138,7 +139,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("completed_estimate_points")[:1]
|
||||
@@ -152,7 +153,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("total_estimate_points")[:1]
|
||||
@@ -166,7 +167,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
backlog_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("backlog_estimate_point")[:1]
|
||||
@@ -180,7 +181,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
unstarted_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("unstarted_estimate_point")[:1]
|
||||
@@ -194,7 +195,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
started_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("started_estimate_point")[:1]
|
||||
@@ -208,7 +209,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
cancelled_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("cancelled_estimate_point")[:1]
|
||||
@@ -270,37 +271,37 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(
|
||||
backlog_estimate_points=Coalesce(
|
||||
Subquery(backlog_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
unstarted_estimate_points=Coalesce(
|
||||
Subquery(unstarted_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
started_estimate_points=Coalesce(
|
||||
Subquery(started_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
cancelled_estimate_points=Coalesce(
|
||||
Subquery(cancelled_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimate_points=Coalesce(
|
||||
Subquery(completed_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
@@ -475,12 +476,12 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -490,7 +491,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -513,12 +514,12 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -528,7 +529,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
|
||||
@@ -47,10 +47,18 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
type = request.GET.get("type", "all")
|
||||
q_filters = Q()
|
||||
|
||||
inbox_issue = Issue.objects.filter(
|
||||
pk=OuterRef("entity_identifier"),
|
||||
issue_inbox__status__in=[0, 2, -2],
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
|
||||
notifications = (
|
||||
Notification.objects.filter(
|
||||
workspace__slug=slug, receiver_id=request.user.id
|
||||
)
|
||||
.filter(entity_name="issue")
|
||||
.annotate(is_inbox_issue=Exists(inbox_issue))
|
||||
.select_related("workspace", "project", "triggered_by", "receiver")
|
||||
.order_by("snoozed_till", "-created_at")
|
||||
)
|
||||
@@ -202,46 +210,19 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
class UnreadNotificationEndpoint(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
# Watching Issues Count
|
||||
subscribed_issues_count = Notification.objects.filter(
|
||||
unread_notifications_count = Notification.objects.filter(
|
||||
workspace__slug=slug,
|
||||
receiver_id=request.user.id,
|
||||
read_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
snoozed_till__isnull=True,
|
||||
entity_identifier__in=IssueSubscriber.objects.filter(
|
||||
workspace__slug=slug, subscriber_id=request.user.id
|
||||
).values_list("issue_id", flat=True),
|
||||
).count()
|
||||
|
||||
# My Issues Count
|
||||
my_issues_count = Notification.objects.filter(
|
||||
workspace__slug=slug,
|
||||
receiver_id=request.user.id,
|
||||
read_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
snoozed_till__isnull=True,
|
||||
entity_identifier__in=IssueAssignee.objects.filter(
|
||||
workspace__slug=slug, assignee_id=request.user.id
|
||||
).values_list("issue_id", flat=True),
|
||||
).count()
|
||||
|
||||
# Created Issues Count
|
||||
created_issues_count = Notification.objects.filter(
|
||||
workspace__slug=slug,
|
||||
receiver_id=request.user.id,
|
||||
read_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
snoozed_till__isnull=True,
|
||||
entity_identifier__in=Issue.objects.filter(
|
||||
workspace__slug=slug, created_by=request.user
|
||||
).values_list("pk", flat=True),
|
||||
).count()
|
||||
|
||||
return Response(
|
||||
{
|
||||
"subscribed_issues": subscribed_issues_count,
|
||||
"my_issues": my_issues_count,
|
||||
"created_issues": created_issues_count,
|
||||
"total_unread_notifications_count": int(
|
||||
unread_notifications_count
|
||||
)
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -44,6 +44,7 @@ from plane.db.models import (
|
||||
WorkspaceTheme,
|
||||
)
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
@@ -118,7 +119,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save(owner=request.user)
|
||||
# Create Workspace member
|
||||
_ = WorkspaceMember.objects.create(
|
||||
@@ -231,7 +232,10 @@ class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.filter(slug=slug).exists()
|
||||
workspace = (
|
||||
Workspace.objects.filter(slug=slug).exists()
|
||||
or slug in RESTRICTED_WORKSPACE_SLUGS
|
||||
)
|
||||
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,16 @@ class OauthAdapter(Adapter):
|
||||
self.client_secret = client_secret
|
||||
self.code = code
|
||||
|
||||
def authentication_error_code(self):
|
||||
if self.provider == "google":
|
||||
return "GOOGLE_OAUTH_PROVIDER_ERROR"
|
||||
elif self.provider == "github":
|
||||
return "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
elif self.provider == "gitlab":
|
||||
return "GITLAB_OAUTH_PROVIDER_ERROR"
|
||||
else:
|
||||
return "OAUTH_NOT_CONFIGURED"
|
||||
|
||||
def get_auth_url(self):
|
||||
return self.auth_url
|
||||
|
||||
@@ -62,7 +72,7 @@ class OauthAdapter(Adapter):
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = self._provider_error_code()
|
||||
code = self.authentication_error_code()
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||
error_message=str(code),
|
||||
@@ -77,15 +87,7 @@ class OauthAdapter(Adapter):
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
if self.provider == "google":
|
||||
code = "GOOGLE_OAUTH_PROVIDER_ERROR"
|
||||
elif self.provider == "github":
|
||||
code = "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
elif self.provider == "gitlab":
|
||||
code = "GITLAB_OAUTH_PROVIDER_ERROR"
|
||||
else:
|
||||
code = "OAUTH_NOT_CONFIGURED"
|
||||
|
||||
code = self.authentication_error_code()
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||
error_message=str(code),
|
||||
|
||||
@@ -91,6 +91,7 @@ class IssueManager(models.Manager):
|
||||
| models.Q(issue_inbox__status=2)
|
||||
| models.Q(issue_inbox__isnull=True)
|
||||
)
|
||||
.filter(state__is_triage=False)
|
||||
.exclude(archived_at__isnull=False)
|
||||
.exclude(project__archived_at__isnull=False)
|
||||
.exclude(is_draft=True)
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.db import models
|
||||
|
||||
# Module imports
|
||||
from .base import BaseModel
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
ROLE_CHOICES = (
|
||||
(20, "Owner"),
|
||||
@@ -112,19 +113,7 @@ def get_issue_props():
|
||||
|
||||
|
||||
def slug_validator(value):
|
||||
if value in [
|
||||
"404",
|
||||
"accounts",
|
||||
"api",
|
||||
"create-workspace",
|
||||
"god-mode",
|
||||
"installations",
|
||||
"invitations",
|
||||
"onboarding",
|
||||
"profile",
|
||||
"spaces",
|
||||
"workspace-invitations",
|
||||
]:
|
||||
if value in RESTRICTED_WORKSPACE_SLUGS:
|
||||
raise ValidationError("Slug is not valid")
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from django.core.management.utils import get_random_secret_key
|
||||
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from corsheaders.defaults import default_headers
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -127,6 +128,8 @@ else:
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
secure_origins = False
|
||||
|
||||
CORS_ALLOW_HEADERS = [*default_headers, "X-API-Key"]
|
||||
|
||||
# Application Settings
|
||||
WSGI_APPLICATION = "plane.wsgi.application"
|
||||
ASGI_APPLICATION = "plane.asgi.application"
|
||||
|
||||
@@ -137,7 +137,7 @@ def burndown_plot(
|
||||
estimate__isnull=False,
|
||||
estimate__type="points",
|
||||
).exists()
|
||||
if estimate_type and plot_type == "points":
|
||||
if estimate_type and plot_type == "points" and cycle_id:
|
||||
issue_estimates = Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -145,7 +145,18 @@ def burndown_plot(
|
||||
estimate_point__isnull=False,
|
||||
).values_list("estimate_point__value", flat=True)
|
||||
|
||||
issue_estimates = [int(value) for value in issue_estimates]
|
||||
issue_estimates = [float(value) for value in issue_estimates]
|
||||
total_estimate_points = sum(issue_estimates)
|
||||
|
||||
if estimate_type and plot_type == "points" and module_id:
|
||||
issue_estimates = Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_module__module_id=module_id,
|
||||
estimate_point__isnull=False,
|
||||
).values_list("estimate_point__value", flat=True)
|
||||
|
||||
issue_estimates = [float(value) for value in issue_estimates]
|
||||
total_estimate_points = sum(issue_estimates)
|
||||
|
||||
if cycle_id:
|
||||
@@ -227,12 +238,12 @@ def burndown_plot(
|
||||
.order_by("date")
|
||||
)
|
||||
|
||||
for date in date_range:
|
||||
if plot_type == "points":
|
||||
if plot_type == "points":
|
||||
for date in date_range:
|
||||
cumulative_pending_issues = total_estimate_points
|
||||
total_completed = 0
|
||||
total_completed = sum(
|
||||
int(item["estimate_point__value"])
|
||||
float(item["estimate_point__value"])
|
||||
for item in completed_issues_estimate_point_distribution
|
||||
if item["date"] is not None and item["date"] <= date
|
||||
)
|
||||
@@ -241,7 +252,8 @@ def burndown_plot(
|
||||
chart_data[str(date)] = None
|
||||
else:
|
||||
chart_data[str(date)] = cumulative_pending_issues
|
||||
else:
|
||||
else:
|
||||
for date in date_range:
|
||||
cumulative_pending_issues = total_issues
|
||||
total_completed = 0
|
||||
total_completed = sum(
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
RESTRICTED_WORKSPACE_SLUGS = [
|
||||
"404",
|
||||
"accounts",
|
||||
"api",
|
||||
"create-workspace",
|
||||
"god-mode",
|
||||
"installations",
|
||||
"invitations",
|
||||
"onboarding",
|
||||
"profile",
|
||||
"spaces",
|
||||
"workspace-invitations",
|
||||
"password",
|
||||
"flags",
|
||||
"monitor",
|
||||
"monitoring",
|
||||
"ingest",
|
||||
"plane-pro",
|
||||
"plane-ultimate",
|
||||
"enterprise",
|
||||
"plane-enterprise",
|
||||
"disco",
|
||||
"silo",
|
||||
"chat",
|
||||
"calendar",
|
||||
"drive",
|
||||
"channels",
|
||||
"upgrade",
|
||||
"billing",
|
||||
]
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"private": true,
|
||||
"main": "./index.ts"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -51,21 +51,6 @@ export const LinkEditView = ({
|
||||
return text;
|
||||
};
|
||||
|
||||
const isValidUrl = (urlString: string) => {
|
||||
const urlPattern = new RegExp(
|
||||
"^(https?:\\/\\/)?" + // validate protocol
|
||||
"([\\w-]+\\.)+[\\w-]{2,}" + // validate domain name
|
||||
"|((\\d{1,3}\\.){3}\\d{1,3})" + // validate IP (v4) address
|
||||
"(\\:\\d+)?(\\/[-\\w.%]+)*" + // validate port and path
|
||||
"(\\?[;&\\w.%=-]*)?" + // validate query string
|
||||
"(\\#[-\\w]*)?$", // validate fragment locator
|
||||
"i"
|
||||
);
|
||||
const regexTest = urlPattern.test(urlString);
|
||||
const urlTest = isValidHttpUrl(urlString); // Ensure you have defined isValidHttpUrl
|
||||
return regexTest && urlTest;
|
||||
};
|
||||
|
||||
const handleUpdateLink = (url: string) => {
|
||||
setLocalUrl(url);
|
||||
};
|
||||
@@ -74,7 +59,7 @@ export const LinkEditView = ({
|
||||
() => () => {
|
||||
if (linkRemoved.current) return;
|
||||
|
||||
const url = isValidUrl(localUrl) ? localUrl : viewProps.url;
|
||||
const url = isValidHttpUrl(localUrl) ? localUrl : viewProps.url;
|
||||
|
||||
if (to >= editor.state.doc.content.size) return;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"private": true,
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"private": true,
|
||||
"main": "./src/index.d.ts"
|
||||
}
|
||||
|
||||
+7
@@ -4,6 +4,7 @@ export type TInstanceAuthenticationModes = {
|
||||
description: string;
|
||||
icon: JSX.Element;
|
||||
config: JSX.Element;
|
||||
unavailable?: boolean;
|
||||
};
|
||||
|
||||
export type TInstanceAuthenticationMethodKeys =
|
||||
@@ -35,3 +36,9 @@ type TInstanceAuthenticationConfigurationKeys =
|
||||
export type TInstanceAuthenticationKeys =
|
||||
| TInstanceAuthenticationMethodKeys
|
||||
| TInstanceAuthenticationConfigurationKeys;
|
||||
|
||||
export type TGetBaseAuthenticationModeProps = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
resolvedTheme: string | undefined;
|
||||
};
|
||||
|
||||
+10
-3
@@ -50,6 +50,7 @@ export type TNotification = {
|
||||
read_at: string | undefined;
|
||||
archived_at: string | undefined;
|
||||
snoozed_till: string | undefined;
|
||||
is_inbox_issue: boolean | undefined;
|
||||
workspace: string | undefined;
|
||||
project: string | undefined;
|
||||
created_at: string | undefined;
|
||||
@@ -84,7 +85,13 @@ export type TNotificationPaginatedInfo = {
|
||||
|
||||
// notification count
|
||||
export type TUnreadNotificationsCount = {
|
||||
created_issues: number | undefined;
|
||||
my_issues: number | undefined;
|
||||
subscribed_issues: number | undefined;
|
||||
total_unread_notifications_count: number;
|
||||
};
|
||||
|
||||
export type TCurrentSelectedNotification = {
|
||||
workspace_slug: string | undefined;
|
||||
project_id: string | undefined;
|
||||
notification_id: string | undefined;
|
||||
issue_id: string | undefined;
|
||||
is_inbox_issue: boolean | undefined;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { FC, useState, useEffect, useCallback } from "react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
|
||||
export type TCollapsibleProps = {
|
||||
title: string | React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
buttonClassName?: string;
|
||||
isOpen?: boolean;
|
||||
onToggle?: () => void;
|
||||
defaultOpen?: boolean;
|
||||
};
|
||||
|
||||
export const Collapsible: FC<TCollapsibleProps> = (props) => {
|
||||
const { title, children, buttonClassName, isOpen, onToggle, defaultOpen } = props;
|
||||
// state
|
||||
const [localIsOpen, setLocalIsOpen] = useState<boolean>(isOpen || defaultOpen ? true : false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen !== undefined) {
|
||||
setLocalIsOpen(isOpen);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// handlers
|
||||
const handleOnClick = useCallback(() => {
|
||||
if (isOpen !== undefined) {
|
||||
if (onToggle) onToggle();
|
||||
} else {
|
||||
setLocalIsOpen((prev) => !prev);
|
||||
}
|
||||
}, [isOpen, onToggle]);
|
||||
|
||||
return (
|
||||
<Disclosure>
|
||||
<Disclosure.Button className={buttonClassName} onClick={handleOnClick}>
|
||||
{title}
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={localIsOpen}
|
||||
className="overflow-hidden"
|
||||
enter="transition-max-height duration-400 ease-in-out"
|
||||
enterFrom="max-h-0"
|
||||
enterTo="max-h-screen"
|
||||
leave="transition-max-height duration-400 ease-in-out"
|
||||
leaveFrom="max-h-screen"
|
||||
leaveTo="max-h-0"
|
||||
>
|
||||
<Disclosure.Panel static>{children}</Disclosure.Panel>
|
||||
</Transition>
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./collapsible";
|
||||
@@ -34,6 +34,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
onMenuClose,
|
||||
tabIndex,
|
||||
closeOnSelect,
|
||||
openOnHover = false,
|
||||
} = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
|
||||
@@ -68,12 +69,24 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
if (closeOnSelect) closeDropdown();
|
||||
};
|
||||
|
||||
const handleMenuButtonClick = (e:React.MouseEvent<HTMLButtonElement, MouseEvent>)=>{
|
||||
const handleMenuButtonClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
isOpen ? closeDropdown() : openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (openOnHover) openDropdown();
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (openOnHover && isOpen) {
|
||||
setTimeout(() => {
|
||||
closeDropdown();
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
@@ -111,6 +124,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
className={cn("relative w-min text-left", className)}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
onClick={handleOnClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
@@ -203,4 +218,4 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||
|
||||
CustomMenu.MenuItem = MenuItem;
|
||||
|
||||
export { CustomMenu };
|
||||
export { CustomMenu };
|
||||
@@ -28,6 +28,7 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
|
||||
onMenuClose?: () => void;
|
||||
closeOnSelect?: boolean;
|
||||
portalElement?: Element | null;
|
||||
openOnHover?: boolean;
|
||||
}
|
||||
|
||||
export interface ICustomSelectProps extends IDropdownProps {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const DropdownIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
|
||||
<svg
|
||||
viewBox="0 0 7 5"
|
||||
className={`${className} stroke-2`}
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...rest}
|
||||
>
|
||||
<path d="M2.77267 4.0211L0.457719 1.78653C0.162864 1.50191 0.0961218 1.17467 0.257492 0.8048C0.418861 0.434934 0.70939 0.25 1.12908 0.25H5.72716C6.14685 0.25 6.43738 0.434934 6.59875 0.8048C6.76012 1.17467 6.69338 1.50191 6.39855 1.78653L4.08357 4.0211C3.98662 4.1147 3.88435 4.18329 3.77676 4.22687C3.66918 4.27046 3.55297 4.29225 3.42813 4.29225C3.30328 4.29225 3.18706 4.27046 3.07948 4.22687C2.97191 4.18329 2.86964 4.1147 2.77267 4.0211Z" />
|
||||
</svg>
|
||||
);
|
||||
@@ -21,3 +21,5 @@ export * from "./related-icon";
|
||||
export * from "./side-panel-icon";
|
||||
export * from "./transfer-icon";
|
||||
export * from "./info-icon";
|
||||
export * from "./relations-icon";
|
||||
export * from "./dropdown-icon";
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const RelationsIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
className={`${className}`}
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...rest}
|
||||
>
|
||||
<path d="M7.99998 4.66536C8.92045 4.66536 9.66665 3.91917 9.66665 2.9987C9.66665 2.07822 8.92045 1.33203 7.99998 1.33203C7.07951 1.33203 6.33331 2.07822 6.33331 2.9987C6.33331 3.91917 7.07951 4.66536 7.99998 4.66536Z" />
|
||||
<path d="M6.80001 4.19922L4.20001 6.79922" />
|
||||
<path d="M2.99998 9.66536C3.92045 9.66536 4.66665 8.91917 4.66665 7.9987C4.66665 7.07822 3.92045 6.33203 2.99998 6.33203C2.07951 6.33203 1.33331 7.07822 1.33331 7.9987C1.33331 8.91917 2.07951 9.66536 2.99998 9.66536Z" />
|
||||
<path d="M4.66669 8H11.3334" />
|
||||
<path d="M13 9.66536C13.9205 9.66536 14.6666 8.91917 14.6666 7.9987C14.6666 7.07822 13.9205 6.33203 13 6.33203C12.0795 6.33203 11.3333 7.07822 11.3333 7.9987C11.3333 8.91917 12.0795 9.66536 13 9.66536Z" />
|
||||
<path d="M9.20001 11.7992L11.8 9.19922" />
|
||||
<path d="M7.99998 14.6654C8.92045 14.6654 9.66665 13.9192 9.66665 12.9987C9.66665 12.0782 8.92045 11.332 7.99998 11.332C7.07951 11.332 6.33331 12.0782 6.33331 12.9987C6.33331 13.9192 7.07951 14.6654 7.99998 14.6654Z" />
|
||||
</svg>
|
||||
);
|
||||
@@ -20,3 +20,4 @@ export * from "./drag-handle";
|
||||
export * from "./drop-indicator";
|
||||
export * from "./favorite-star";
|
||||
export * from "./loader";
|
||||
export * from "./collapsible";
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ModalCore: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={() => handleClose && handleClose}>
|
||||
<Dialog as="div" className="relative z-20" onClose={() => handleClose && handleClose()}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
|
||||
@@ -87,7 +87,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options className="absolute left-0 z-10 mt-1 min-w-[8rem] origin-top-left overflow-y-auto whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none">
|
||||
<Listbox.Options className="absolute left-0 z-10 mt-1 min-w-[12rem] origin-top-left overflow-y-auto whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none">
|
||||
<div className="space-y-1 p-2">
|
||||
{PEEK_MODES.map((mode) => (
|
||||
<Listbox.Option
|
||||
@@ -95,24 +95,14 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
value={mode.key}
|
||||
className={({ active, selected }) =>
|
||||
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon
|
||||
iconName={mode.icon}
|
||||
className={`-my-1 flex-shrink-0 !text-base ${mode.key === "full" ? "rotate-45" : ""}`}
|
||||
/>
|
||||
{mode.label}
|
||||
</div>
|
||||
</div>
|
||||
{selected && <Icon iconName="done" />}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
|
||||
{mode.label}
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { WorkspaceActiveCyclesUpgrade } from "@/components/workspace";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { WorkspaceActiveCyclesRoot } from "@/plane-web/components/active-cycles";
|
||||
|
||||
const WorkspaceActiveCyclesPage = observer(() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -15,7 +16,7 @@ const WorkspaceActiveCyclesPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<WorkspaceActiveCyclesUpgrade />
|
||||
<WorkspaceActiveCyclesRoot />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,19 +3,25 @@
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { InboxContentRoot } from "@/components/inbox";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
// constants
|
||||
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
|
||||
// hooks
|
||||
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
|
||||
const WorkspaceDashboardPage = observer(() => {
|
||||
// hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
|
||||
const { currentSelectedNotification, notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
|
||||
const {
|
||||
membership: { fetchUserProjectInfo },
|
||||
} = useUser();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined;
|
||||
const { workspace_slug, project_id, issue_id, is_inbox_issue } = currentSelectedNotification;
|
||||
|
||||
// fetch workspace notifications
|
||||
const notificationMutation =
|
||||
@@ -29,15 +35,42 @@ const WorkspaceDashboardPage = observer(() => {
|
||||
useSWR(
|
||||
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null,
|
||||
currentWorkspace?.slug
|
||||
? async () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
|
||||
? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
|
||||
: null
|
||||
);
|
||||
|
||||
// fetching user project member info
|
||||
const { isLoading: projectMemberInfoLoader } = useSWR(
|
||||
workspace_slug && project_id && is_inbox_issue
|
||||
? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}`
|
||||
: null,
|
||||
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto">
|
||||
<IssuePeekOverview embedIssue />
|
||||
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
|
||||
<>
|
||||
{projectMemberInfoLoader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
) : (
|
||||
<InboxContentRoot
|
||||
setIsMobileSidebar={() => {}}
|
||||
isMobileSidebar={false}
|
||||
workspaceSlug={workspace_slug}
|
||||
projectId={project_id}
|
||||
inboxIssueId={issue_id}
|
||||
isNotificationEmbed
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<IssuePeekOverview embedIssue />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
ISSUE_LAYOUTS,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel } from "@/hooks/store";
|
||||
|
||||
@@ -108,8 +108,6 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="flex justify-evenly border-b border-custom-border-200 py-2 md:hidden">
|
||||
<CustomMenu
|
||||
@@ -146,7 +144,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
|
||||
+6
-3
@@ -24,7 +24,7 @@ import {
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
@@ -152,7 +152,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
return (
|
||||
@@ -248,7 +247,11 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
+9
-5
@@ -12,9 +12,15 @@ import { CustomMenu } from "@plane/ui";
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import {
|
||||
EIssueFilterType,
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
||||
ISSUE_LAYOUTS,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
|
||||
|
||||
@@ -109,8 +115,6 @@ export const CycleIssuesMobileHeader = () => {
|
||||
[workspaceSlug, projectId, cycleId, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -150,7 +154,7 @@ export const CycleIssuesMobileHeader = () => {
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
+1
-1
@@ -98,4 +98,4 @@ const ProjectCyclesPage = observer(() => {
|
||||
);
|
||||
});
|
||||
|
||||
export default ProjectCyclesPage;
|
||||
export default ProjectCyclesPage;
|
||||
|
||||
+2
-4
@@ -18,7 +18,7 @@ import {
|
||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
@@ -90,8 +90,6 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
: currentProjectDetails.draft_issues
|
||||
: undefined;
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@@ -140,7 +138,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
+5
-6
@@ -22,8 +22,8 @@ import {
|
||||
} from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { SPACE_BASE_URL } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import {
|
||||
useEventTracker,
|
||||
@@ -107,13 +107,12 @@ export const ProjectIssuesHeader = observer(() => {
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const publishedURL = `${SPACE_BASE_URL}/issues/${currentProjectDetails?.anchor}`;
|
||||
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
|
||||
const publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
return (
|
||||
@@ -193,7 +192,7 @@ export const ProjectIssuesHeader = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
+9
-5
@@ -13,9 +13,15 @@ import { CustomMenu } from "@plane/ui";
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import {
|
||||
EIssueFilterType,
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
||||
ISSUE_LAYOUTS,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
@@ -88,8 +94,6 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -129,7 +133,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
+6
-3
@@ -24,7 +24,7 @@ import {
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
@@ -152,7 +152,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
return (
|
||||
@@ -249,7 +248,11 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
+9
-5
@@ -13,9 +13,15 @@ import { CustomMenu } from "@plane/ui";
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import {
|
||||
EIssueFilterType,
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
||||
ISSUE_LAYOUTS,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
@@ -90,8 +96,6 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="block md:hidden">
|
||||
<ProjectAnalyticsModal
|
||||
@@ -131,7 +135,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
+2
-4
@@ -22,7 +22,7 @@ import {
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
@@ -133,8 +133,6 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -233,7 +231,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
disabled={!canUserCreateIssue}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
@@ -34,7 +34,7 @@ const GlobalViewIssuesPage = observer(() => {
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewsHeader />
|
||||
{globalViewId && <GlobalViewsAppliedFiltersRoot globalViewId={globalViewId.toString()} />}
|
||||
<AllIssueLayoutRoot />
|
||||
<AllIssueLayoutRoot isDefaultView={!!defaultView} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useLabel, useMember, useUser, useIssues, useGlobalView } from "@/hooks/store";
|
||||
|
||||
@@ -98,7 +98,6 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
const isLocked = viewDetails?.is_locked;
|
||||
|
||||
return (
|
||||
@@ -118,7 +117,11 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
<div className="flex items-center gap-2">
|
||||
{!isLocked && (
|
||||
<>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./root";
|
||||
export * from "./workspace-active-cycles-upgrade";
|
||||
@@ -0,0 +1,3 @@
|
||||
import { WorkspaceActiveCyclesUpgrade } from "@/plane-web/components/active-cycles";
|
||||
|
||||
export const WorkspaceActiveCyclesRoot = () => <WorkspaceActiveCyclesUpgrade />;
|
||||
@@ -139,11 +139,6 @@ export const CommandPalette: FC = observer(() => {
|
||||
description: "Create a new issue in the current project",
|
||||
action: () => toggleCreateIssueModal(true),
|
||||
},
|
||||
h: {
|
||||
title: "Show shortcuts",
|
||||
description: "Show all the available shortcuts",
|
||||
action: () => toggleShortcutModal(true),
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
p: {
|
||||
@@ -199,16 +194,18 @@ export const CommandPalette: FC = observer(() => {
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const { key, ctrlKey, metaKey, altKey } = e;
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
|
||||
if (!key) return;
|
||||
|
||||
const keyPressed = key.toLowerCase();
|
||||
const cmdClicked = ctrlKey || metaKey;
|
||||
const shiftClicked = shiftKey;
|
||||
|
||||
if (cmdClicked && keyPressed === "k" && !isAnyModalOpen) {
|
||||
e.preventDefault();
|
||||
toggleCommandPaletteModal(true);
|
||||
}
|
||||
|
||||
// if on input, textarea or editor, don't do anything
|
||||
if (
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
@@ -217,6 +214,11 @@ export const CommandPalette: FC = observer(() => {
|
||||
)
|
||||
return;
|
||||
|
||||
if (shiftClicked && (keyPressed === "?" || keyPressed === "/") && !isAnyModalOpen) {
|
||||
e.preventDefault();
|
||||
toggleShortcutModal(true);
|
||||
}
|
||||
|
||||
if (cmdClicked) {
|
||||
if (keyPressed === "c" && ((platform === "MacOS" && ctrlKey) || altKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ShortcutCommandsList: React.FC<Props> = (props) => {
|
||||
{ keys: "V", description: "Create view" },
|
||||
{ keys: "D", description: "Create page" },
|
||||
{ keys: "Delete", description: "Bulk delete issues" },
|
||||
{ keys: "H", description: "Open shortcuts guide" },
|
||||
{ keys: "Shift,/", description: "Open shortcuts guide" },
|
||||
{
|
||||
keys: platform === "MacOS" ? "Ctrl,control,C" : "Ctrl,Alt,C",
|
||||
description: "Copy issue URL from the issue details page",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { mutate } from "swr";
|
||||
// types
|
||||
import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types";
|
||||
// ui
|
||||
@@ -43,6 +44,16 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
const selectedProjectId = payload.project_id ?? projectId.toString();
|
||||
await createCycle(workspaceSlug, selectedProjectId, payload)
|
||||
.then((res) => {
|
||||
// mutate when the current cycle creation is active
|
||||
if (payload.start_date && payload.end_date) {
|
||||
const currentDate = new Date();
|
||||
const cycleStartDate = new Date(payload.start_date);
|
||||
const cycleEndDate = new Date(payload.end_date);
|
||||
if (currentDate >= cycleStartDate && currentDate <= cycleEndDate) {
|
||||
mutate(`PROJECT_ACTIVE_CYCLE_${selectedProjectId}`);
|
||||
}
|
||||
}
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
|
||||
@@ -236,7 +236,14 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
<Combobox.Option key={option.value} value={option.value}>
|
||||
{({ active, selected }) => (
|
||||
<div
|
||||
className={`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${active ? `!hover:bg-custom-background-80` : ``} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
|
||||
{
|
||||
"bg-custom-background-80": active,
|
||||
"text-custom-text-100": selected,
|
||||
"text-custom-text-200": !selected,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="flex-grow truncate">{option.content}</span>
|
||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||
|
||||
@@ -39,6 +39,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||
showAccessSpecifier = false,
|
||||
showSubmitButton = true,
|
||||
isSubmitting = false,
|
||||
placeholder = "Add comment...",
|
||||
...rest
|
||||
} = props;
|
||||
// store hooks
|
||||
@@ -81,8 +82,9 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||
highlights: mentionHighlights,
|
||||
suggestions: mentionSuggestions,
|
||||
}}
|
||||
{...rest}
|
||||
placeholder={placeholder}
|
||||
containerClassName={cn(containerClassName, "relative")}
|
||||
{...rest}
|
||||
/>
|
||||
<IssueCommentToolbar
|
||||
accessSpecifier={accessSpecifier}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
|
||||
ganttContainerRef,
|
||||
isPaginating ? null : intersectionElement,
|
||||
loadMoreBlocks,
|
||||
"50% 0% 50% 0%"
|
||||
"100% 0% 100% 0%"
|
||||
);
|
||||
|
||||
const handleOnDrop = (
|
||||
|
||||
@@ -16,44 +16,44 @@ import {
|
||||
VideoIcon,
|
||||
} from "@/components/icons/attachment";
|
||||
|
||||
export const getFileIcon = (fileType: string) => {
|
||||
export const getFileIcon = (fileType: string, size: number = 28) => {
|
||||
switch (fileType) {
|
||||
case "pdf":
|
||||
return <PdfIcon height={28} width={28} />;
|
||||
return <PdfIcon height={size} width={size} />;
|
||||
case "csv":
|
||||
return <CsvIcon height={28} width={28} />;
|
||||
return <CsvIcon height={size} width={size} />;
|
||||
case "xlsx":
|
||||
return <SheetIcon height={28} width={28} />;
|
||||
return <SheetIcon height={size} width={size} />;
|
||||
case "css":
|
||||
return <CssIcon height={28} width={28} />;
|
||||
return <CssIcon height={size} width={size} />;
|
||||
case "doc":
|
||||
return <DocIcon height={28} width={28} />;
|
||||
return <DocIcon height={size} width={size} />;
|
||||
case "fig":
|
||||
return <FigmaIcon height={28} width={28} />;
|
||||
return <FigmaIcon height={size} width={size} />;
|
||||
case "html":
|
||||
return <HtmlIcon height={28} width={28} />;
|
||||
return <HtmlIcon height={size} width={size} />;
|
||||
case "png":
|
||||
return <PngIcon height={28} width={28} />;
|
||||
return <PngIcon height={size} width={size} />;
|
||||
case "jpg":
|
||||
return <JpgIcon height={28} width={28} />;
|
||||
return <JpgIcon height={size} width={size} />;
|
||||
case "js":
|
||||
return <JavaScriptIcon height={28} width={28} />;
|
||||
return <JavaScriptIcon height={size} width={size} />;
|
||||
case "txt":
|
||||
return <TxtIcon height={28} width={28} />;
|
||||
return <TxtIcon height={size} width={size} />;
|
||||
case "svg":
|
||||
return <SvgIcon height={28} width={28} />;
|
||||
return <SvgIcon height={size} width={size} />;
|
||||
case "mp3":
|
||||
return <AudioIcon height={28} width={28} />;
|
||||
return <AudioIcon height={size} width={size} />;
|
||||
case "wav":
|
||||
return <AudioIcon height={28} width={28} />;
|
||||
return <AudioIcon height={size} width={size} />;
|
||||
case "mp4":
|
||||
return <VideoIcon height={28} width={28} />;
|
||||
return <VideoIcon height={size} width={size} />;
|
||||
case "wmv":
|
||||
return <VideoIcon height={28} width={28} />;
|
||||
return <VideoIcon height={size} width={size} />;
|
||||
case "mkv":
|
||||
return <VideoIcon height={28} width={28} />;
|
||||
return <VideoIcon height={size} width={size} />;
|
||||
|
||||
default:
|
||||
return <DefaultIcon height={28} width={28} />;
|
||||
return <DefaultIcon height={size} width={size} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,10 +44,19 @@ type TInboxIssueActionsHeader = {
|
||||
isSubmitting: "submitting" | "submitted" | "saved";
|
||||
isMobileSidebar: boolean;
|
||||
setIsMobileSidebar: (value: boolean) => void;
|
||||
isNotificationEmbed: boolean;
|
||||
};
|
||||
|
||||
export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxIssue, isSubmitting, isMobileSidebar, setIsMobileSidebar } = props;
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
inboxIssue,
|
||||
isSubmitting,
|
||||
isMobileSidebar,
|
||||
setIsMobileSidebar,
|
||||
isNotificationEmbed = false,
|
||||
} = props;
|
||||
// states
|
||||
const [isSnoozeDateModalOpen, setIsSnoozeDateModalOpen] = useState(false);
|
||||
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
|
||||
@@ -58,7 +67,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
const { currentTab, deleteInboxIssue, filteredInboxIssueIds } = useProjectInbox();
|
||||
const { data: currentUser } = useUser();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
membership: { currentProjectRoleByProjectId },
|
||||
} = useUser();
|
||||
|
||||
const router = useAppRouter();
|
||||
@@ -66,6 +75,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
|
||||
const issue = inboxIssue?.issue;
|
||||
// derived values
|
||||
const currentProjectRole = currentProjectRoleByProjectId(projectId) || undefined;
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
const canMarkAsDuplicate = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
|
||||
const canMarkAsAccepted = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
|
||||
@@ -91,11 +101,13 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
};
|
||||
|
||||
const handleRedirection = (nextOrPreviousIssueId: string | undefined) => {
|
||||
if (nextOrPreviousIssueId)
|
||||
router.push(
|
||||
`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}&inboxIssueId=${nextOrPreviousIssueId}`
|
||||
);
|
||||
else router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
|
||||
if (!isNotificationEmbed) {
|
||||
if (nextOrPreviousIssueId)
|
||||
router.push(
|
||||
`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}&inboxIssueId=${nextOrPreviousIssueId}`
|
||||
);
|
||||
else router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInboxIssueAccept = async () => {
|
||||
@@ -126,7 +138,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
const handleInboxIssueDelete = async () => {
|
||||
if (!inboxIssue || !currentInboxIssueId) return;
|
||||
await deleteInboxIssue(workspaceSlug, projectId, currentInboxIssueId).finally(() => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox`);
|
||||
if (!isNotificationEmbed) router.push(`/${workspaceSlug}/projects/${projectId}/inbox`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -179,11 +191,11 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
if (!isNotificationEmbed) document.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
if (!isNotificationEmbed) document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [onKeyDown]);
|
||||
}, [onKeyDown, isNotificationEmbed]);
|
||||
|
||||
if (!inboxIssue) return null;
|
||||
|
||||
@@ -240,22 +252,24 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 p-1.5"
|
||||
onClick={() => handleInboxIssueNavigation("prev")}
|
||||
>
|
||||
<ChevronUp size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 p-1.5"
|
||||
onClick={() => handleInboxIssueNavigation("next")}
|
||||
>
|
||||
<ChevronDown size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
{!isNotificationEmbed && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 p-1.5"
|
||||
onClick={() => handleInboxIssueNavigation("prev")}
|
||||
>
|
||||
<ChevronUp size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 p-1.5"
|
||||
onClick={() => handleInboxIssueNavigation("next")}
|
||||
>
|
||||
<ChevronDown size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{canMarkAsAccepted && (
|
||||
@@ -313,12 +327,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
{isAllowed && (
|
||||
<CustomMenu verticalEllipsis placement="bottom-start">
|
||||
{canMarkAsAccepted && (
|
||||
<CustomMenu.MenuItem onClick={handleIssueSnoozeAction}>
|
||||
<CustomMenu.MenuItem onClick={handleIssueSnoozeAction}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={14} strokeWidth={2} />
|
||||
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0
|
||||
? "Un-snooze"
|
||||
: "Snooze"}
|
||||
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0
|
||||
? "Un-snooze"
|
||||
: "Snooze"}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
||||
@@ -15,10 +15,18 @@ type TInboxContentRoot = {
|
||||
inboxIssueId: string;
|
||||
isMobileSidebar: boolean;
|
||||
setIsMobileSidebar: (value: boolean) => void;
|
||||
isNotificationEmbed?: boolean;
|
||||
};
|
||||
|
||||
export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxIssueId, isMobileSidebar, setIsMobileSidebar } = props;
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
inboxIssueId,
|
||||
isMobileSidebar,
|
||||
setIsMobileSidebar,
|
||||
isNotificationEmbed = false,
|
||||
} = props;
|
||||
/// router
|
||||
const router = useAppRouter();
|
||||
// states
|
||||
@@ -33,11 +41,11 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isIssueAvailable && inboxIssueId) {
|
||||
if (!isIssueAvailable && inboxIssueId && !isNotificationEmbed) {
|
||||
router.replace(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isIssueAvailable]);
|
||||
}, [isIssueAvailable, isNotificationEmbed]);
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && inboxIssueId
|
||||
@@ -69,6 +77,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||
projectId={projectId}
|
||||
inboxIssue={inboxIssue}
|
||||
isSubmitting={isSubmitting}
|
||||
isNotificationEmbed={isNotificationEmbed || false}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto px-6 py-5 vertical-scrollbar scrollbar-md">
|
||||
|
||||
@@ -9,7 +9,7 @@ import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/com
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
@@ -63,8 +63,6 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
};
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="group relative flex border-b border-custom-border-200">
|
||||
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
||||
@@ -72,7 +70,7 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
</div>
|
||||
{/* filter options */}
|
||||
<div className="flex items-center gap-2 px-8">
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters || {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { AlertCircle, X } from "lucide-react";
|
||||
@@ -35,9 +35,9 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
|
||||
const { getUserDetails } = useMember();
|
||||
const {
|
||||
attachment: { getAttachmentById },
|
||||
isDeleteAttachmentModalOpen,
|
||||
toggleDeleteAttachmentModal,
|
||||
} = useIssueDetail();
|
||||
// state
|
||||
const [isDeleteIssueAttachmentModalOpen, setIsDeleteIssueAttachmentModalOpen] = useState(false);
|
||||
// derived values
|
||||
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
|
||||
// hooks
|
||||
@@ -47,10 +47,10 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDeleteAttachmentModalOpen === attachment.id && (
|
||||
{isDeleteIssueAttachmentModalOpen && (
|
||||
<IssueAttachmentDeleteModal
|
||||
isOpen={!!isDeleteAttachmentModalOpen}
|
||||
onClose={() => toggleDeleteAttachmentModal(null)}
|
||||
isOpen={isDeleteIssueAttachmentModalOpen}
|
||||
onClose={() => setIsDeleteIssueAttachmentModalOpen(false)}
|
||||
handleAttachmentOperations={handleAttachmentOperations}
|
||||
data={attachment}
|
||||
/>
|
||||
@@ -58,7 +58,7 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
|
||||
<div className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-custom-border-200 bg-custom-background-100 px-4 py-2 text-sm">
|
||||
<Link href={attachment.asset} target="_blank" rel="noopener noreferrer">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div>
|
||||
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset), 28)}</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip tooltipContent={getFileName(attachment.attributes.name)} isMobile={isMobile}>
|
||||
@@ -85,7 +85,7 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
|
||||
</Link>
|
||||
|
||||
{!disabled && (
|
||||
<button type="button" onClick={() => toggleDeleteAttachmentModal(attachment.id)}>
|
||||
<button type="button" onClick={() => setIsDeleteIssueAttachmentModalOpen(true)}>
|
||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { FC, useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { UploadCloud } from "lucide-react";
|
||||
// hooks
|
||||
import { MAX_FILE_SIZE } from "@/constants/common";
|
||||
import { generateFileName } from "@/helpers/attachment.helper";
|
||||
import { useInstance, useIssueDetail } from "@/hooks/store";
|
||||
// components
|
||||
import { IssueAttachmentsListItem } from "./attachment-list-item";
|
||||
// types
|
||||
import { TAttachmentOperations } from "./root";
|
||||
|
||||
type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
||||
|
||||
type TIssueAttachmentItemList = {
|
||||
workspaceSlug: string;
|
||||
issueId: string;
|
||||
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((props) => {
|
||||
const { workspaceSlug, issueId, handleAttachmentOperations, disabled } = props;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// store hooks
|
||||
const { config } = useInstance();
|
||||
const {
|
||||
attachment: { getAttachmentsByIssueId },
|
||||
} = useIssueDetail();
|
||||
// derived values
|
||||
const issueAttachments = getAttachmentsByIssueId(issueId);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
const currentFile: File = acceptedFiles[0];
|
||||
if (!currentFile || !workspaceSlug) return;
|
||||
|
||||
const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), {
|
||||
type: currentFile.type,
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append("asset", uploadedFile);
|
||||
formData.append(
|
||||
"attributes",
|
||||
JSON.stringify({
|
||||
name: uploadedFile.name,
|
||||
size: uploadedFile.size,
|
||||
})
|
||||
);
|
||||
setIsLoading(true);
|
||||
handleAttachmentOperations.create(formData).finally(() => setIsLoading(false));
|
||||
},
|
||||
[handleAttachmentOperations, workspaceSlug]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
maxSize: config?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
multiple: false,
|
||||
disabled: isLoading || disabled,
|
||||
});
|
||||
|
||||
if (!issueAttachments) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`relative flex flex-col ${isDragActive && issueAttachments.length < 3 ? "min-h-[200px]" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive && (
|
||||
<div className="absolute flex items-center justify-center left-0 top-0 h-full w-full bg-custom-background-90/75 z-30 ">
|
||||
<div className="flex items-center justify-center p-1 rounded-md bg-custom-background-100">
|
||||
<div className="flex flex-col justify-center items-center px-5 py-6 rounded-md border border-dashed border-custom-border-300">
|
||||
<UploadCloud className="size-7" />
|
||||
<span className="text-sm text-custom-text-300">Drag and drop anywhere to upload</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issueAttachments?.map((attachmentId) => (
|
||||
<IssueAttachmentsListItem
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
handleAttachmentOperations={handleAttachmentOperations}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trash } from "lucide-react";
|
||||
// ui
|
||||
import { CustomMenu, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import { getFileIcon } from "@/components/icons";
|
||||
import { IssueAttachmentDeleteModal } from "@/components/issues";
|
||||
// helpers
|
||||
import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useMember } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// types
|
||||
import { TAttachmentOperations } from "./root";
|
||||
|
||||
type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
||||
|
||||
type TIssueAttachmentsListItem = {
|
||||
attachmentId: string;
|
||||
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer((props) => {
|
||||
// props
|
||||
const { attachmentId, handleAttachmentOperations, disabled } = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
const {
|
||||
attachment: { getAttachmentById },
|
||||
} = useIssueDetail();
|
||||
// state
|
||||
const [isDeleteIssueAttachmentModalOpen, setIsDeleteIssueAttachmentModalOpen] = useState(false);
|
||||
|
||||
// derived values
|
||||
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
if (!attachment) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDeleteIssueAttachmentModalOpen && (
|
||||
<IssueAttachmentDeleteModal
|
||||
isOpen={isDeleteIssueAttachmentModalOpen}
|
||||
onClose={() => setIsDeleteIssueAttachmentModalOpen(false)}
|
||||
handleAttachmentOperations={handleAttachmentOperations}
|
||||
data={attachment}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(attachment.asset, "_blank");
|
||||
}}
|
||||
>
|
||||
<div className="group flex items-center justify-between gap-3 h-11 hover:bg-custom-background-90 pl-9 pr-2">
|
||||
<div className="flex items-center gap-3 text-sm truncate">
|
||||
<div className="flex items-center gap-3 ">{getFileIcon(getFileExtension(attachment.asset), 18)}</div>
|
||||
<Tooltip
|
||||
tooltipContent={`${getFileName(attachment.attributes.name)}.${getFileExtension(attachment.asset)}`}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<p className="text-custom-text-200 font-medium truncate">{`${getFileName(attachment.attributes.name)}.${getFileExtension(attachment.asset)}`}</p>
|
||||
</Tooltip>
|
||||
<span className="flex size-1.5 bg-custom-background-80 rounded-full" />
|
||||
<span className="flex-shrink-0 text-custom-text-400">{convertBytesToSize(attachment.attributes.size)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{attachment?.updated_by && (
|
||||
<>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${
|
||||
getUserDetails(attachment.updated_by)?.display_name ?? ""
|
||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<ButtonAvatars showTooltip userIds={attachment?.updated_by} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CustomMenu ellipsis closeOnSelect placement="bottom-end" openOnHover disabled={disabled}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDeleteIssueAttachmentModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from "./attachment-detail";
|
||||
export * from "./attachment-item-list";
|
||||
export * from "./attachment-list-item";
|
||||
export * from "./attachment-upload";
|
||||
export * from "./attachments-list";
|
||||
export * from "./delete-attachment-modal";
|
||||
|
||||
@@ -2,3 +2,5 @@ export * from "./root";
|
||||
|
||||
export * from "./links";
|
||||
export * from "./link-detail";
|
||||
export * from "./link-item";
|
||||
export * from "./link-list";
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { Pencil, Trash2, LinkIcon, ExternalLink } from "lucide-react";
|
||||
// ui
|
||||
import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui";
|
||||
// helpers
|
||||
import { calculateTimeAgoShort } from "@/helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal";
|
||||
|
||||
type TIssueLinkItem = {
|
||||
linkId: string;
|
||||
linkOperations: TLinkOperationsModal;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const IssueLinkItem: FC<TIssueLinkItem> = (props) => {
|
||||
// props
|
||||
const { linkId, linkOperations, isNotAllowed } = props;
|
||||
// hooks
|
||||
const {
|
||||
toggleIssueLinkModal: toggleIssueLinkModalStore,
|
||||
link: { getLinkById },
|
||||
} = useIssueDetail();
|
||||
|
||||
// state
|
||||
const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false);
|
||||
const toggleIssueLinkModal = (modalToggle: boolean) => {
|
||||
toggleIssueLinkModalStore(modalToggle);
|
||||
setIsIssueLinkModalOpen(modalToggle);
|
||||
};
|
||||
const { isMobile } = usePlatformOS();
|
||||
const linkDetail = getLinkById(linkId);
|
||||
if (!linkDetail) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssueLinkCreateUpdateModal
|
||||
isModalOpen={isIssueLinkModalOpen}
|
||||
handleModal={toggleIssueLinkModal}
|
||||
linkOperations={linkOperations}
|
||||
preloadedData={linkDetail}
|
||||
/>
|
||||
<div
|
||||
key={linkId}
|
||||
className="col-span-12 lg:col-span-6 xl:col-span-4 2xl:col-span-3 3xl:col-span-2 flex items-center justify-between gap-3 h-8 flex-shrink-0 px-3 bg-custom-background-90 border-[0.5px] border-custom-border-200 rounded"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 truncate">
|
||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
||||
<Tooltip tooltipContent={linkDetail.url} isMobile={isMobile}>
|
||||
<span
|
||||
className="truncate text-xs cursor-pointer"
|
||||
onClick={() => {
|
||||
copyTextToClipboard(linkDetail.url);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link copied!",
|
||||
message: "Link copied to clipboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="p-1 text-xs align-bottom leading-5 text-custom-text-300">
|
||||
{calculateTimeAgoShort(linkDetail.created_at)}
|
||||
</p>
|
||||
<a
|
||||
href={linkDetail.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative grid place-items-center rounded p-1 text-custom-text-300 outline-none hover:text-custom-text-200 cursor-pointer hover:bg-custom-background-80"
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
</a>
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
buttonClassName="text-custom-text-300 hover:text-custom-text-200"
|
||||
placement="bottom-end"
|
||||
closeOnSelect
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleIssueLinkModal(true);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
||||
Edit
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
linkOperations.remove(linkDetail.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// computed
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { IssueLinkItem } from "./link-item";
|
||||
// hooks
|
||||
import { TLinkOperations } from "./root";
|
||||
|
||||
type TLinkOperationsModal = Exclude<TLinkOperations, "create">;
|
||||
|
||||
type TLinkList = {
|
||||
issueId: string;
|
||||
linkOperations: TLinkOperationsModal;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const LinkList: FC<TLinkList> = observer((props) => {
|
||||
// props
|
||||
const { issueId, linkOperations, disabled = false } = props;
|
||||
// hooks
|
||||
const {
|
||||
link: { getLinksByIssueId },
|
||||
} = useIssueDetail();
|
||||
|
||||
const issueLinks = getLinksByIssueId(issueId);
|
||||
|
||||
if (!issueLinks) return <></>;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12 3xl:grid-cols-10 gap-2 px-9 py-4">
|
||||
{issueLinks &&
|
||||
issueLinks.length > 0 &&
|
||||
issueLinks.map((linkId) => (
|
||||
<IssueLinkItem key={linkId} linkId={linkId} linkOperations={linkOperations} isNotAllowed={disabled} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// computed
|
||||
import { useIssueDetail, useUser } from "@/hooks/store";
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { IssueLinkDetail } from "./link-detail";
|
||||
// hooks
|
||||
import { TLinkOperations } from "./root";
|
||||
@@ -11,18 +11,16 @@ export type TLinkOperationsModal = Exclude<TLinkOperations, "create">;
|
||||
export type TIssueLinkList = {
|
||||
issueId: string;
|
||||
linkOperations: TLinkOperationsModal;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueLinkList: FC<TIssueLinkList> = observer((props) => {
|
||||
// props
|
||||
const { issueId, linkOperations } = props;
|
||||
const { issueId, linkOperations, disabled = false } = props;
|
||||
// hooks
|
||||
const {
|
||||
link: { getLinksByIssueId },
|
||||
} = useIssueDetail();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const issueLinks = getLinksByIssueId(issueId);
|
||||
|
||||
@@ -33,12 +31,7 @@ export const IssueLinkList: FC<TIssueLinkList> = observer((props) => {
|
||||
{issueLinks &&
|
||||
issueLinks.length > 0 &&
|
||||
issueLinks.map((linkId) => (
|
||||
<IssueLinkDetail
|
||||
key={linkId}
|
||||
linkId={linkId}
|
||||
linkOperations={linkOperations}
|
||||
isNotAllowed={currentProjectRole === 5 || currentProjectRole === 10}
|
||||
/>
|
||||
<IssueLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} isNotAllowed={disabled} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -126,7 +126,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<IssueLinkList issueId={issueId} linkOperations={handleLinkOperations} />
|
||||
<IssueLinkList issueId={issueId} linkOperations={handleLinkOperations} disabled={disabled} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -47,11 +47,8 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
|
||||
modulesToAdd.push(moduleId);
|
||||
}
|
||||
}
|
||||
if (modulesToRemove.length > 0)
|
||||
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, modulesToRemove);
|
||||
|
||||
if (modulesToAdd.length > 0)
|
||||
await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, modulesToAdd);
|
||||
await issueOperations.changeModulesInIssue?.(workspaceSlug, projectId, issueId, modulesToAdd, modulesToRemove);
|
||||
|
||||
setIsUpdating(false);
|
||||
};
|
||||
|
||||
@@ -32,18 +32,18 @@ export type TIssueOperations = {
|
||||
addCycleToIssue?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
|
||||
removeIssueFromModule?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
issueId: string
|
||||
) => Promise<void>;
|
||||
removeModulesFromIssue?: (
|
||||
changeModulesInIssue?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
moduleIds: string[]
|
||||
addModuleIds: string[],
|
||||
removeModuleIds: string[]
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -70,9 +70,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
addCycleToIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
addModulesToIssue,
|
||||
changeModulesInIssue,
|
||||
removeIssueFromModule,
|
||||
removeModulesFromIssue,
|
||||
} = useIssueDetail();
|
||||
const {
|
||||
issues: { removeIssue: removeArchivedIssue },
|
||||
@@ -258,35 +257,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||
try {
|
||||
const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: "module_id",
|
||||
change_details: moduleIds,
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be added to the module. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: "module_id",
|
||||
change_details: moduleIds,
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
}
|
||||
},
|
||||
removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
|
||||
try {
|
||||
const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||
@@ -323,25 +293,24 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
removeModulesFromIssue: async (
|
||||
changeModulesInIssue: async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
moduleIds: string[]
|
||||
addModuleIds: string[],
|
||||
removeModuleIds: string[]
|
||||
) => {
|
||||
const removeModulesFromIssuePromise = removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||
setPromiseToast(removeModulesFromIssuePromise, {
|
||||
loading: "Removing module from issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Module removed from issue successfully",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Module remove from issue failed",
|
||||
const promise = await changeModulesInIssue(workspaceSlug, projectId, issueId, addModuleIds, removeModuleIds);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: "module_id",
|
||||
change_details: { addModuleIds, removeModuleIds },
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
await removeModulesFromIssuePromise;
|
||||
return promise;
|
||||
},
|
||||
}),
|
||||
[
|
||||
@@ -353,9 +322,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
removeArchivedIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
addModulesToIssue,
|
||||
changeModulesInIssue,
|
||||
removeIssueFromModule,
|
||||
removeModulesFromIssue,
|
||||
captureIssueEvent,
|
||||
pathname,
|
||||
]
|
||||
|
||||
@@ -224,7 +224,8 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||
classNames="space-y-2 px-3 py-2"
|
||||
root={scrollableContainerRef}
|
||||
defaultHeight="100px"
|
||||
horizontalOffset={50}
|
||||
horizontalOffset={100}
|
||||
verticalOffset={200}
|
||||
>
|
||||
<KanbanIssueDetailsBlock
|
||||
cardRef={cardRef}
|
||||
|
||||
@@ -125,6 +125,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
|
||||
defaultHeight="3rem"
|
||||
root={containerRef}
|
||||
classNames={`relative ${isLastChild && !isExpanded ? "" : "border-b border-b-custom-border-200"}`}
|
||||
verticalOffset={100}
|
||||
>
|
||||
<IssueBlock
|
||||
issueId={issueId}
|
||||
|
||||
@@ -102,7 +102,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults;
|
||||
const isPaginating = !!getIssueLoader(group.id);
|
||||
|
||||
useIntersectionObserver(containerRef, isPaginating ? null : intersectionElement, loadMoreIssues, `50% 0% 50% 0%`);
|
||||
useIntersectionObserver(containerRef, isPaginating ? null : intersectionElement, loadMoreIssues, `100% 0% 100% 0%`);
|
||||
|
||||
const shouldLoadMore =
|
||||
nextPageResults === undefined && groupIssueCount !== undefined && groupIssueIds
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user