Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f04220818c | ||
|
|
98562a55c7 |
@@ -2,7 +2,7 @@ name: Bug report
|
||||
description: Create a bug report to help us improve Plane
|
||||
title: "[bug]: "
|
||||
labels: [🐛bug]
|
||||
assignees: [vihar, pushya22]
|
||||
assignees: [srinivaspendem, pushya22]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Feature request
|
||||
description: Suggest a feature to improve Plane
|
||||
title: "[feature]: "
|
||||
labels: [✨feature]
|
||||
assignees: [vihar, pushya22]
|
||||
assignees: [srinivaspendem, pushya22]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -2,11 +2,6 @@ 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 }}
|
||||
@@ -27,29 +22,27 @@ 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
|
||||
@@ -61,6 +54,7 @@ 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 }}
|
||||
@@ -102,6 +96,7 @@ 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,10 +13,6 @@ on:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
base_tag_name:
|
||||
description: 'Base Tag Name'
|
||||
required: false
|
||||
default: ''
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
@@ -31,7 +27,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
|
||||
flat_branch_name: ${{ steps.set_env_variables.outputs.FLAT_BRANCH_NAME }}
|
||||
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
|
||||
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
|
||||
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
|
||||
@@ -57,9 +52,7 @@ jobs:
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ github.event_name}}" == "workflow_dispatch" ] && [ "${{ 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
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
echo "AIO_BASE_TAG=preview" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
@@ -79,9 +72,6 @@ jobs:
|
||||
echo "DO_SLIM_BUILD=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
FLAT_BRANCH_NAME=$(echo "${{ env.TARGET_BRANCH }}" | sed 's/[^a-zA-Z0-9]/-/g')
|
||||
echo "FLAT_BRANCH_NAME=$FLAT_BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
@@ -93,7 +83,7 @@ jobs:
|
||||
env:
|
||||
BUILD_TYPE: full
|
||||
AIO_BASE_TAG: ${{ needs.branch_build_setup.outputs.aio_base_tag }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:full-${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:full-${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
@@ -136,7 +126,7 @@ jobs:
|
||||
tags: ${{ env.AIO_IMAGE_TAGS }}
|
||||
push: true
|
||||
build-args: |
|
||||
BASE_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TYPE=${{env.BUILD_TYPE}}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -153,7 +143,7 @@ jobs:
|
||||
env:
|
||||
BUILD_TYPE: slim
|
||||
AIO_BASE_TAG: ${{ needs.branch_build_setup.outputs.aio_base_tag }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:slim-${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:slim-${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
@@ -196,7 +186,7 @@ jobs:
|
||||
tags: ${{ env.AIO_IMAGE_TAGS }}
|
||||
push: true
|
||||
build-args: |
|
||||
BASE_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TYPE=${{env.BUILD_TYPE}}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -2,11 +2,6 @@ name: Feature Preview
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_tag_name:
|
||||
description: 'Base Tag Name'
|
||||
required: false
|
||||
default: 'preview'
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
@@ -34,12 +29,7 @@ jobs:
|
||||
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $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 "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AuthenticationMethodCard } from "@/components/authentication";
|
||||
import { UpgradeButton } from "@/components/common/upgrade-button";
|
||||
import { getBaseAuthenticationModes } from "@/helpers/authentication.helper";
|
||||
// images
|
||||
import OIDCLogo from "@/public/logos/oidc-logo.svg";
|
||||
import OIDCLogo from "@/public/logos/oidc-logo.png";
|
||||
import SAMLLogo from "@/public/logos/saml-logo.svg";
|
||||
|
||||
export type TAuthenticationModeProps = {
|
||||
@@ -32,7 +32,7 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
|
||||
key: "oidc",
|
||||
name: "OIDC",
|
||||
description: "Authenticate your users via the OpenID Connect protocol.",
|
||||
icon: <Image src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
|
||||
icon: <Image src={OIDCLogo} height={20} width={20} alt="OIDC Logo" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
@@ -40,7 +40,7 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
|
||||
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" />,
|
||||
icon: <Image src={SAMLLogo} height={24} width={24} alt="SAML Logo" className="pb-0.5 pl-0.5" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -1,11 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 702 B |
@@ -1,17 +1,16 @@
|
||||
<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"/>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
|
||||
<g fill="#C22E33">
|
||||
|
||||
<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
|
||||
|
||||
<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
|
||||
|
||||
<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
|
||||
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3262_5767">
|
||||
<rect width="700" height="650" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.22.0"
|
||||
"version": "0.21.0"
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ class CycleSerializer(BaseSerializer):
|
||||
started_issues = serializers.IntegerField(read_only=True)
|
||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||
backlog_issues = serializers.IntegerField(read_only=True)
|
||||
total_estimates = serializers.FloatField(read_only=True)
|
||||
completed_estimates = serializers.FloatField(read_only=True)
|
||||
started_estimates = serializers.FloatField(read_only=True)
|
||||
total_estimates = serializers.IntegerField(read_only=True)
|
||||
completed_estimates = serializers.IntegerField(read_only=True)
|
||||
started_estimates = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
|
||||
@@ -11,7 +11,6 @@ from rest_framework import serializers
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueType,
|
||||
IssueActivity,
|
||||
IssueAssignee,
|
||||
IssueAttachment,
|
||||
@@ -132,12 +131,7 @@ class IssueSerializer(BaseSerializer):
|
||||
workspace_id = self.context["workspace_id"]
|
||||
default_assignee_id = self.context["default_assignee_id"]
|
||||
|
||||
# Get the issue type from the project
|
||||
issue_type = IssueType.objects.filter(project_id=project_id).first()
|
||||
|
||||
issue = Issue.objects.create(
|
||||
**validated_data, project_id=project_id, type=issue_type
|
||||
)
|
||||
issue = Issue.objects.create(**validated_data, project_id=project_id)
|
||||
|
||||
# Issue Audit Users
|
||||
created_by_id = issue.created_by_id
|
||||
@@ -318,14 +312,10 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
return IssueLink.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if (
|
||||
IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
)
|
||||
.exclude(pk=instance.id)
|
||||
.exists()
|
||||
):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
).exclude(pk=instance.id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.db.models import (
|
||||
OuterRef,
|
||||
Q,
|
||||
Sum,
|
||||
FloatField,
|
||||
IntegerField,
|
||||
)
|
||||
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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -1140,38 +1140,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
updated_cycles = []
|
||||
update_cycle_issue_activity = []
|
||||
for cycle_issue in cycle_issues:
|
||||
cycle_issue.cycle_id = new_cycle_id
|
||||
updated_cycles.append(cycle_issue)
|
||||
update_cycle_issue_activity.append(
|
||||
{
|
||||
"old_cycle_id": str(cycle_id),
|
||||
"new_cycle_id": str(new_cycle_id),
|
||||
"issue_id": str(cycle_issue.issue_id),
|
||||
}
|
||||
)
|
||||
|
||||
cycle_issues = CycleIssue.objects.bulk_update(
|
||||
updated_cycles, ["cycle_id"], batch_size=100
|
||||
)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.created",
|
||||
requested_data=json.dumps({"cycles_list": []}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": "[]",
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -16,7 +16,6 @@ from plane.app.permissions import ProjectLitePermission
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Inbox,
|
||||
IssueType,
|
||||
InboxIssue,
|
||||
Issue,
|
||||
Project,
|
||||
@@ -142,8 +141,6 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
color="#ff7700",
|
||||
is_triage=True,
|
||||
)
|
||||
# Get the issue type
|
||||
issue_type = IssueType.objects.filter(project_id=project_id).first()
|
||||
|
||||
# create an issue
|
||||
issue = Issue.objects.create(
|
||||
@@ -152,10 +149,9 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
description_html=request.data.get("issue", {}).get(
|
||||
"description_html", "<p></p>"
|
||||
),
|
||||
priority=request.data.get("issue", {}).get("priority", "none"),
|
||||
priority=request.data.get("issue", {}).get("priority", "low"),
|
||||
project_id=project_id,
|
||||
state=state,
|
||||
type=issue_type,
|
||||
)
|
||||
|
||||
# create an inbox issue
|
||||
|
||||
@@ -26,7 +26,6 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
State,
|
||||
Workspace,
|
||||
IssueType,
|
||||
)
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from .base import BaseAPIView
|
||||
@@ -241,14 +240,6 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
.filter(pk=serializer.data["id"])
|
||||
.first()
|
||||
)
|
||||
|
||||
# Create the Issue Types
|
||||
IssueType.objects.create(
|
||||
name="Task",
|
||||
description="A task that needs to be done",
|
||||
project_id=project.id,
|
||||
)
|
||||
|
||||
# Model activity
|
||||
model_activity.delay(
|
||||
model_name="project",
|
||||
|
||||
@@ -91,7 +91,6 @@ from .page import (
|
||||
PageLogSerializer,
|
||||
SubPageSerializer,
|
||||
PageDetailSerializer,
|
||||
PageVersionSerializer,
|
||||
)
|
||||
|
||||
from .estimate import (
|
||||
|
||||
@@ -33,7 +33,6 @@ from plane.db.models import (
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
State,
|
||||
IssueType,
|
||||
)
|
||||
|
||||
|
||||
@@ -136,12 +135,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
workspace_id = self.context["workspace_id"]
|
||||
default_assignee_id = self.context["default_assignee_id"]
|
||||
|
||||
# Get Issue Type
|
||||
issue_type = IssueType.objects.filter(project_id=project_id).first()
|
||||
# Create Issue
|
||||
issue = Issue.objects.create(
|
||||
**validated_data, project_id=project_id, type=issue_type
|
||||
)
|
||||
issue = Issue.objects.create(**validated_data, project_id=project_id)
|
||||
|
||||
# Issue Audit Users
|
||||
created_by_id = issue.created_by_id
|
||||
@@ -465,14 +459,10 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
return IssueLink.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if (
|
||||
IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
)
|
||||
.exclude(pk=instance.id)
|
||||
.exists()
|
||||
):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
).exclude(pk=instance.id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
@@ -519,7 +509,7 @@ class IssueAttachmentLiteSerializer(DynamicBaseSerializer):
|
||||
"attributes",
|
||||
"issue_id",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"updated_by_id",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -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.FloatField(read_only=True)
|
||||
completed_estimate_points = serializers.FloatField(read_only=True)
|
||||
total_estimate_points = serializers.IntegerField(read_only=True)
|
||||
completed_estimate_points = serializers.IntegerField(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.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)
|
||||
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)
|
||||
|
||||
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,15 +3,11 @@ 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
|
||||
|
||||
@@ -10,7 +10,6 @@ from plane.db.models import (
|
||||
Label,
|
||||
ProjectPage,
|
||||
Project,
|
||||
PageVersion,
|
||||
)
|
||||
|
||||
|
||||
@@ -162,13 +161,3 @@ class PageLogSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"page",
|
||||
]
|
||||
|
||||
|
||||
class PageVersionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = PageVersion
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"page",
|
||||
]
|
||||
|
||||
@@ -15,7 +15,6 @@ from plane.db.models import (
|
||||
WorkspaceTheme,
|
||||
WorkspaceUserProperties,
|
||||
)
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
@@ -23,11 +22,22 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
|
||||
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
|
||||
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"})
|
||||
|
||||
class Meta:
|
||||
model = Workspace
|
||||
|
||||
@@ -7,7 +7,6 @@ from plane.app.views import (
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
PageVersionEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -91,14 +90,4 @@ urlpatterns = [
|
||||
),
|
||||
name="page-description",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/versions/",
|
||||
PageVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/versions/<uuid:pk>/",
|
||||
PageVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -179,7 +179,6 @@ from .page.base import (
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
)
|
||||
from .page.version import PageVersionEndpoint
|
||||
|
||||
from .search.base import GlobalSearchEndpoint
|
||||
from .search.issue import IssueSearchEndpoint
|
||||
|
||||
@@ -19,7 +19,7 @@ from django.db.models import (
|
||||
When,
|
||||
Subquery,
|
||||
Sum,
|
||||
FloatField,
|
||||
IntegerField,
|
||||
)
|
||||
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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.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=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
unstarted_estimate_points=Coalesce(
|
||||
Subquery(unstarted_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
started_estimate_points=Coalesce(
|
||||
Subquery(started_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
cancelled_estimate_points=Coalesce(
|
||||
Subquery(cancelled_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimate_points=Coalesce(
|
||||
Subquery(completed_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -465,14 +465,14 @@ class CycleViewSet(BaseViewSet):
|
||||
}
|
||||
|
||||
if data[0]["start_date"] and data[0]["end_date"]:
|
||||
data[0]["estimate_distribution"][
|
||||
"completion_chart"
|
||||
] = burndown_plot(
|
||||
queryset=queryset.first(),
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type="points",
|
||||
cycle_id=data[0]["id"],
|
||||
data[0]["estimate_distribution"]["completion_chart"] = (
|
||||
burndown_plot(
|
||||
queryset=queryset.first(),
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type="points",
|
||||
cycle_id=data[0]["id"],
|
||||
)
|
||||
)
|
||||
|
||||
assignee_distribution = (
|
||||
@@ -836,12 +836,12 @@ class CycleViewSet(BaseViewSet):
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
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", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -1507,40 +1507,14 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
updated_cycles = []
|
||||
update_cycle_issue_activity = []
|
||||
for cycle_issue in cycle_issues:
|
||||
cycle_issue.cycle_id = new_cycle_id
|
||||
updated_cycles.append(cycle_issue)
|
||||
update_cycle_issue_activity.append(
|
||||
{
|
||||
"old_cycle_id": str(cycle_id),
|
||||
"new_cycle_id": str(new_cycle_id),
|
||||
"issue_id": str(cycle_issue.issue_id),
|
||||
}
|
||||
)
|
||||
|
||||
cycle_issues = CycleIssue.objects.bulk_update(
|
||||
updated_cycles, ["cycle_id"], batch_size=100
|
||||
)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.created",
|
||||
requested_data=json.dumps({"cycles_list": []}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": "[]",
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -542,7 +542,6 @@ def dashboard_recent_collaborators(self, request, slug):
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__archived_at__isnull=True,
|
||||
is_active=True,
|
||||
)
|
||||
.annotate(
|
||||
num_activities=Coalesce(
|
||||
|
||||
@@ -3,11 +3,8 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q, OuterRef, F, Func, UUIDField, Value, CharField
|
||||
from django.db.models import Q
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -23,9 +20,6 @@ from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
IssueRelation,
|
||||
Issue,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
@@ -67,149 +61,56 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
# get all blocking issues
|
||||
|
||||
blocking_issues = issue_relations.filter(
|
||||
relation_type="blocked_by", related_issue_id=issue_id
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all blocked by issues
|
||||
)
|
||||
blocked_by_issues = issue_relations.filter(
|
||||
relation_type="blocked_by", issue_id=issue_id
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
# get all duplicate issues
|
||||
)
|
||||
duplicate_issues = issue_relations.filter(
|
||||
issue_id=issue_id, relation_type="duplicate"
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
# get all relates to issues
|
||||
)
|
||||
duplicate_issues_related = issue_relations.filter(
|
||||
related_issue_id=issue_id, relation_type="duplicate"
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all relates to issues
|
||||
)
|
||||
relates_to_issues = issue_relations.filter(
|
||||
issue_id=issue_id, relation_type="relates_to"
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
# get all relates to issues
|
||||
)
|
||||
relates_to_issues_related = issue_relations.filter(
|
||||
related_issue_id=issue_id, relation_type="relates_to"
|
||||
).values_list("issue_id", flat=True)
|
||||
)
|
||||
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).distinct()
|
||||
blocked_by_issues_serialized = IssueRelationSerializer(
|
||||
blocked_by_issues, many=True
|
||||
).data
|
||||
duplicate_issues_serialized = IssueRelationSerializer(
|
||||
duplicate_issues, many=True
|
||||
).data
|
||||
relates_to_issues_serialized = IssueRelationSerializer(
|
||||
relates_to_issues, many=True
|
||||
).data
|
||||
|
||||
# Fields
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"priority",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"relation_type",
|
||||
]
|
||||
# revere relation for blocked by issues
|
||||
blocking_issues_serialized = RelatedIssueSerializer(
|
||||
blocking_issues, many=True
|
||||
).data
|
||||
# reverse relation for duplicate issues
|
||||
duplicate_issues_related_serialized = RelatedIssueSerializer(
|
||||
duplicate_issues_related, many=True
|
||||
).data
|
||||
# reverse relation for related issues
|
||||
relates_to_issues_related_serialized = RelatedIssueSerializer(
|
||||
relates_to_issues_related, many=True
|
||||
).data
|
||||
|
||||
response_data = {
|
||||
"blocking": queryset.filter(pk__in=blocking_issues)
|
||||
.annotate(
|
||||
relation_type=Value("blocking", output_field=CharField())
|
||||
)
|
||||
.values(*fields),
|
||||
"blocked_by": queryset.filter(pk__in=blocked_by_issues)
|
||||
.annotate(
|
||||
relation_type=Value("blocked_by", output_field=CharField())
|
||||
)
|
||||
.values(*fields),
|
||||
"duplicate": queryset.filter(pk__in=duplicate_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"duplicate",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields)
|
||||
| queryset.filter(pk__in=duplicate_issues_related)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"duplicate",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"relates_to": queryset.filter(pk__in=relates_to_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"relates_to",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields)
|
||||
| queryset.filter(pk__in=relates_to_issues_related)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"relates_to",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"blocking": blocking_issues_serialized,
|
||||
"blocked_by": blocked_by_issues_serialized,
|
||||
"duplicate": duplicate_issues_serialized
|
||||
+ duplicate_issues_related_serialized,
|
||||
"relates_to": relates_to_issues_serialized
|
||||
+ relates_to_issues_related_serialized,
|
||||
}
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -99,7 +99,6 @@ 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,7 +17,6 @@ from django.db.models import (
|
||||
UUIDField,
|
||||
Value,
|
||||
Sum,
|
||||
FloatField,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
@@ -139,7 +138,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("completed_estimate_points")[:1]
|
||||
@@ -153,7 +152,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("total_estimate_points")[:1]
|
||||
@@ -167,7 +166,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
backlog_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("backlog_estimate_point")[:1]
|
||||
@@ -181,7 +180,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
unstarted_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("unstarted_estimate_point")[:1]
|
||||
@@ -195,7 +194,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
started_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("started_estimate_point")[:1]
|
||||
@@ -209,7 +208,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
cancelled_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("cancelled_estimate_point")[:1]
|
||||
@@ -271,37 +270,37 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(
|
||||
backlog_estimate_points=Coalesce(
|
||||
Subquery(backlog_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
unstarted_estimate_points=Coalesce(
|
||||
Subquery(unstarted_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
started_estimate_points=Coalesce(
|
||||
Subquery(started_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
cancelled_estimate_points=Coalesce(
|
||||
Subquery(cancelled_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimate_points=Coalesce(
|
||||
Subquery(completed_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
@@ -476,12 +475,12 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -491,7 +490,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -514,12 +513,12 @@ class ModuleViewSet(BaseViewSet):
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -529,7 +528,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
|
||||
@@ -45,21 +45,12 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
archived = request.GET.get("archived", "false")
|
||||
read = request.GET.get("read", None)
|
||||
type = request.GET.get("type", "all")
|
||||
mentioned = request.GET.get("mentioned", False)
|
||||
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")
|
||||
)
|
||||
@@ -87,13 +78,6 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
if read == "true":
|
||||
notifications = notifications.filter(read_at__isnull=False)
|
||||
|
||||
if mentioned:
|
||||
notifications = notifications.filter(sender__icontains="mentioned")
|
||||
else:
|
||||
notifications = notifications.exclude(
|
||||
sender__icontains="mentioned"
|
||||
)
|
||||
|
||||
type = type.split(",")
|
||||
# Subscribed issues
|
||||
if "subscribed" in type:
|
||||
@@ -218,35 +202,46 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
class UnreadNotificationEndpoint(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
# Watching Issues Count
|
||||
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,
|
||||
)
|
||||
.exclude(sender__icontains="mentioned")
|
||||
.count()
|
||||
)
|
||||
|
||||
mention_notifications_count = Notification.objects.filter(
|
||||
subscribed_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,
|
||||
sender__icontains="mentioned",
|
||||
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(
|
||||
{
|
||||
"total_unread_notifications_count": int(
|
||||
unread_notifications_count
|
||||
),
|
||||
"mention_unread_notifications_count": int(
|
||||
mention_notifications_count
|
||||
),
|
||||
"subscribed_issues": subscribed_issues_count,
|
||||
"my_issues": my_issues_count,
|
||||
"created_issues": created_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -38,7 +38,6 @@ from plane.db.models import (
|
||||
from ..base import BaseAPIView, BaseViewSet
|
||||
|
||||
from plane.bgtasks.page_transaction_task import page_transaction
|
||||
from plane.bgtasks.page_version_task import page_version
|
||||
|
||||
|
||||
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
||||
@@ -216,14 +215,8 @@ class PageViewSet(BaseViewSet):
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
issue_ids = PageLog.objects.filter(
|
||||
page_id=pk, entity_name="issue"
|
||||
).values_list("entity_identifier", flat=True)
|
||||
data = PageDetailSerializer(page).data
|
||||
data["issue_ids"] = issue_ids
|
||||
return Response(
|
||||
data,
|
||||
status=status.HTTP_200_OK,
|
||||
PageDetailSerializer(page).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def lock(self, request, slug, project_id, pk):
|
||||
@@ -482,38 +475,16 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
status=472,
|
||||
)
|
||||
|
||||
# Serialize the existing instance
|
||||
existing_instance = json.dumps(
|
||||
{
|
||||
"description_html": page.description_html,
|
||||
},
|
||||
cls=DjangoJSONEncoder,
|
||||
)
|
||||
|
||||
# Get the base64 data from the request
|
||||
base64_data = request.data.get("description_binary")
|
||||
|
||||
# If base64 data is provided
|
||||
if base64_data:
|
||||
# Decode the base64 data to bytes
|
||||
new_binary_data = base64.b64decode(base64_data)
|
||||
# capture the page transaction
|
||||
if request.data.get("description_html"):
|
||||
page_transaction.delay(
|
||||
new_value=request.data,
|
||||
old_value=existing_instance,
|
||||
page_id=pk,
|
||||
)
|
||||
|
||||
# Store the updated binary data
|
||||
page.description_binary = new_binary_data
|
||||
page.description_html = request.data.get("description_html")
|
||||
page.save()
|
||||
# Return a success response
|
||||
page_version.delay(
|
||||
page_id=page.id,
|
||||
existing_instance=existing_instance,
|
||||
user_id=request.user.id,
|
||||
)
|
||||
return Response({"message": "Updated successfully"})
|
||||
else:
|
||||
return Response({"error": "No binary data provided"})
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import PageVersion
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.serializers import PageVersionSerializer
|
||||
|
||||
|
||||
class PageVersionEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id, page_id, pk=None):
|
||||
# Check if pk is provided
|
||||
if pk:
|
||||
# Return a single page version
|
||||
page_version = PageVersion.objects.get(
|
||||
workspace__slug=slug,
|
||||
page_id=page_id,
|
||||
pk=pk,
|
||||
)
|
||||
# Serialize the page version
|
||||
serializer = PageVersionSerializer(page_version)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
# Return all page versions
|
||||
page_versions = PageVersion.objects.filter(
|
||||
workspace__slug=slug,
|
||||
page_id=page_id,
|
||||
)
|
||||
# Serialize the page versions
|
||||
serializer = PageVersionSerializer(page_versions, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -47,7 +47,6 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
State,
|
||||
Workspace,
|
||||
IssueType,
|
||||
)
|
||||
from plane.utils.cache import cache_response
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
@@ -343,13 +342,6 @@ class ProjectViewSet(BaseViewSet):
|
||||
.first()
|
||||
)
|
||||
|
||||
# Create the issue type
|
||||
IssueType.objects.create(
|
||||
name="Task",
|
||||
description="A task that needs to be done",
|
||||
project_id=project.id,
|
||||
)
|
||||
|
||||
model_activity.delay(
|
||||
model_name="project",
|
||||
model_id=str(project.id),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Python imports
|
||||
import re
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Q
|
||||
@@ -10,7 +11,13 @@ from rest_framework.response import Response
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
Project,
|
||||
Issue,
|
||||
Cycle,
|
||||
Module,
|
||||
Page,
|
||||
IssueView,
|
||||
)
|
||||
from plane.utils.issue_search import search_issues
|
||||
|
||||
|
||||
@@ -37,9 +37,6 @@ from plane.utils.paginator import BasePaginator
|
||||
from plane.authentication.utils.host import user_ip
|
||||
from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
|
||||
from plane.utils.host import base_host
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
|
||||
|
||||
class UserEndpoint(BaseViewSet):
|
||||
@@ -50,8 +47,6 @@ class UserEndpoint(BaseViewSet):
|
||||
return self.request.user
|
||||
|
||||
@cache_response(60 * 60)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def retrieve(self, request):
|
||||
serialized_data = UserMeSerializer(request.user).data
|
||||
return Response(
|
||||
@@ -60,8 +55,6 @@ class UserEndpoint(BaseViewSet):
|
||||
)
|
||||
|
||||
@cache_response(60 * 60)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def retrieve_user_settings(self, request):
|
||||
serialized_data = UserMeSettingsSerializer(request.user).data
|
||||
return Response(serialized_data, status=status.HTTP_200_OK)
|
||||
@@ -86,9 +79,6 @@ class UserEndpoint(BaseViewSet):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(path="/api/users/me/")
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", multiple=True, user=False
|
||||
)
|
||||
def deactivate(self, request):
|
||||
# Check all workspace user is active
|
||||
user = self.get_object()
|
||||
@@ -298,8 +288,6 @@ class AccountEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class ProfileEndpoint(BaseAPIView):
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def get(self, request):
|
||||
profile = Profile.objects.get(user=request.user)
|
||||
serializer = ProfileSerializer(profile)
|
||||
|
||||
@@ -44,10 +44,6 @@ from plane.db.models import (
|
||||
WorkspaceTheme,
|
||||
)
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
@@ -122,7 +118,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
if serializer.is_valid():
|
||||
serializer.save(owner=request.user)
|
||||
# Create Workspace member
|
||||
_ = WorkspaceMember.objects.create(
|
||||
@@ -175,8 +171,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
@cache_response(60 * 60 * 2)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def get(self, request):
|
||||
fields = [
|
||||
field
|
||||
@@ -237,10 +231,7 @@ class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = (
|
||||
Workspace.objects.filter(slug=slug).exists()
|
||||
or slug in RESTRICTED_WORKSPACE_SLUGS
|
||||
)
|
||||
workspace = Workspace.objects.filter(slug=slug).exists()
|
||||
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -39,16 +39,6 @@ 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
|
||||
|
||||
@@ -72,7 +62,7 @@ class OauthAdapter(Adapter):
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = self.authentication_error_code()
|
||||
code = self._provider_error_code()
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||
error_message=str(code),
|
||||
@@ -87,7 +77,15 @@ class OauthAdapter(Adapter):
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = self.authentication_error_code()
|
||||
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"
|
||||
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||
error_message=str(code),
|
||||
|
||||
@@ -21,7 +21,6 @@ from plane.db.models import (
|
||||
Cycle,
|
||||
Module,
|
||||
Issue,
|
||||
IssueType,
|
||||
IssueSequence,
|
||||
IssueAssignee,
|
||||
IssueLabel,
|
||||
@@ -337,12 +336,6 @@ def create_issues(workspace, project, user_id, issue_count):
|
||||
65535 if largest_sort_order is None else largest_sort_order + 10000
|
||||
)
|
||||
|
||||
issue_type = IssueType.objects.create(
|
||||
name="Task",
|
||||
description="A task that needs to be completed.",
|
||||
project=project,
|
||||
)
|
||||
|
||||
for _ in range(0, issue_count):
|
||||
start_date = [None, fake.date_this_year()][random.randint(0, 1)]
|
||||
end_date = (
|
||||
@@ -371,7 +364,6 @@ def create_issues(workspace, project, user_id, issue_count):
|
||||
random.randint(0, 4)
|
||||
],
|
||||
created_by_id=creators[random.randint(0, len(creators) - 1)],
|
||||
type=issue_type,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Page, PageVersion
|
||||
|
||||
|
||||
@shared_task
|
||||
def page_version(
|
||||
page_id,
|
||||
existing_instance,
|
||||
user_id,
|
||||
):
|
||||
# Get the page
|
||||
page = Page.objects.get(id=page_id)
|
||||
|
||||
# Get the current instance
|
||||
current_instance = (
|
||||
json.loads(existing_instance) if existing_instance is not None else {}
|
||||
)
|
||||
|
||||
# Create a version if description_html is updated
|
||||
if current_instance.get("description_html") != page.description_html:
|
||||
# Create a new page version
|
||||
PageVersion.objects.create(
|
||||
page_id=page_id,
|
||||
workspace_id=page.workspace_id,
|
||||
description_html=page.description_html,
|
||||
description_binary=page.description_binary,
|
||||
ownned_by_id=user_id,
|
||||
last_saved_at=page.updated_at,
|
||||
)
|
||||
|
||||
# If page versions are greater than 20 delete the oldest one
|
||||
if PageVersion.objects.filter(page_id=page_id).count() > 20:
|
||||
# Delete the old page version
|
||||
PageVersion.objects.filter(page_id=page_id).order_by(
|
||||
"last_saved_at"
|
||||
).first().delete()
|
||||
|
||||
return
|
||||
@@ -1,275 +0,0 @@
|
||||
# Generated by Django 4.2.11 on 2024-07-01 06:10
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
def create_issue_types(apps, schema_editor):
|
||||
Project = apps.get_model("db", "Project")
|
||||
Issue = apps.get_model("db", "Issue")
|
||||
IssueType = apps.get_model("db", "IssueType")
|
||||
# Create the issue types for all projects
|
||||
IssueType.objects.bulk_create(
|
||||
[
|
||||
IssueType(
|
||||
name="Task",
|
||||
description="A task that needs to be completed.",
|
||||
project_id=project["id"],
|
||||
workspace_id=project["workspace_id"],
|
||||
)
|
||||
for project in Project.objects.values("id", "workspace_id")
|
||||
],
|
||||
batch_size=1000,
|
||||
)
|
||||
# Update the issue type for all existing issues
|
||||
issue_types = {
|
||||
str(issue_type["project_id"]): str(issue_type["id"])
|
||||
for issue_type in IssueType.objects.values("id", "project_id")
|
||||
}
|
||||
# Update the issue type for all existing issues
|
||||
bulk_issues = []
|
||||
for issue in Issue.objects.all():
|
||||
issue.type_id = issue_types[str(issue.project_id)]
|
||||
bulk_issues.append(issue)
|
||||
|
||||
# Update the issue type for all existing issues
|
||||
Issue.objects.bulk_update(bulk_issues, ["type_id"], batch_size=1000)
|
||||
|
||||
|
||||
def create_page_versions(apps, schema_editor):
|
||||
Page = apps.get_model("db", "Page")
|
||||
PageVersion = apps.get_model("db", "PageVersion")
|
||||
# Create the page versions for all pages
|
||||
PageVersion.objects.bulk_create(
|
||||
[
|
||||
PageVersion(
|
||||
page_id=page["id"],
|
||||
workspace_id=page["workspace_id"],
|
||||
description_html=page["description_html"],
|
||||
description_binary=page["description_binary"],
|
||||
description_stripped=page["description_stripped"],
|
||||
ownned_by_id=page["owned_by_id"],
|
||||
last_saved_at=page["updated_at"],
|
||||
)
|
||||
for page in Page.objects.values(
|
||||
"id",
|
||||
"workspace_id",
|
||||
"description_html",
|
||||
"description_binary",
|
||||
"description_stripped",
|
||||
"owned_by_id",
|
||||
"updated_at",
|
||||
)
|
||||
],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0069_alter_account_provider_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="IssueType",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("logo_props", models.JSONField(default=dict)),
|
||||
("sort_order", models.FloatField(default=65535)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_%(class)s",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_%(class)s",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_default",
|
||||
models.BooleanField(default=True),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Issue Type",
|
||||
"verbose_name_plural": "Issue Types",
|
||||
"db_table": "issue_types",
|
||||
"ordering": ("sort_order",),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issue",
|
||||
name="type",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="issue_type",
|
||||
to="db.issuetype",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="apitoken",
|
||||
name="is_service",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.RunPython(create_issue_types),
|
||||
migrations.CreateModel(
|
||||
name="PageVersion",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_saved_at",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("description_binary", models.BinaryField(null=True)),
|
||||
(
|
||||
"description_html",
|
||||
models.TextField(blank=True, default="<p></p>"),
|
||||
),
|
||||
(
|
||||
"description_stripped",
|
||||
models.TextField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"ownned_by",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="page_versions",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"page",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="page_versions",
|
||||
to="db.page",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="page_versions",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Page Version",
|
||||
"verbose_name_plural": "Page Versions",
|
||||
"db_table": "page_versions",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="project",
|
||||
name="start_date",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="project",
|
||||
name="target_date",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.RunPython(create_page_versions),
|
||||
]
|
||||
@@ -50,14 +50,7 @@ from .notification import (
|
||||
Notification,
|
||||
UserNotificationPreference,
|
||||
)
|
||||
from .page import (
|
||||
Page,
|
||||
PageFavorite,
|
||||
PageLabel,
|
||||
PageLog,
|
||||
ProjectPage,
|
||||
PageVersion,
|
||||
)
|
||||
from .page import Page, PageFavorite, PageLabel, PageLog, ProjectPage
|
||||
from .project import (
|
||||
Project,
|
||||
ProjectBaseModel,
|
||||
@@ -108,5 +101,3 @@ from .webhook import Webhook, WebhookLog
|
||||
from .dashboard import Dashboard, DashboardWidget, Widget
|
||||
|
||||
from .favorite import UserFavorite
|
||||
|
||||
from .issue_type import IssueType
|
||||
|
||||
@@ -44,7 +44,6 @@ class APIToken(BaseModel):
|
||||
null=True,
|
||||
)
|
||||
expired_at = models.DateTimeField(blank=True, null=True)
|
||||
is_service = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "API Token"
|
||||
|
||||
@@ -91,7 +91,6 @@ 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)
|
||||
@@ -164,13 +163,6 @@ class Issue(ProjectBaseModel):
|
||||
is_draft = models.BooleanField(default=False)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
type = models.ForeignKey(
|
||||
"db.IssueType",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="issue_type",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
issue_objects = IssueManager()
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
|
||||
# Module imports
|
||||
from .workspace import WorkspaceBaseModel
|
||||
|
||||
|
||||
class IssueType(WorkspaceBaseModel):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
logo_props = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
is_default = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Type"
|
||||
verbose_name_plural = "Issue Types"
|
||||
db_table = "issue_types"
|
||||
ordering = ("sort_order",)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# If we are adding a new issue type, we need to set the sort order
|
||||
if self._state.adding:
|
||||
# Get the largest sort order for the project
|
||||
largest_sort_order = IssueType.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
# If there are issue types, set the sort order to the largest + 10000
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
super(IssueType, self).save(*args, **kwargs)
|
||||
@@ -1,7 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
@@ -67,15 +66,6 @@ class Page(BaseModel):
|
||||
"""Return owner email and page name"""
|
||||
return f"{self.owned_by.email} <{self.name}>"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(Page, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class PageLog(BaseModel):
|
||||
TYPE_CHOICES = (
|
||||
@@ -103,9 +93,7 @@ class PageLog(BaseModel):
|
||||
verbose_name="Transaction Type",
|
||||
)
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workspace_page_log",
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -259,40 +247,3 @@ class TeamPage(BaseModel):
|
||||
verbose_name_plural = "Team Pages"
|
||||
db_table = "team_pages"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class PageVersion(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="page_versions",
|
||||
)
|
||||
page = models.ForeignKey(
|
||||
"db.Page",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="page_versions",
|
||||
)
|
||||
last_saved_at = models.DateTimeField(default=timezone.now)
|
||||
ownned_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="page_versions",
|
||||
)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Page Version"
|
||||
verbose_name_plural = "Page Versions"
|
||||
db_table = "page_versions"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(PageVersion, self).save(*args, **kwargs)
|
||||
|
||||
@@ -115,9 +115,6 @@ class Project(BaseModel):
|
||||
related_name="default_state",
|
||||
)
|
||||
archived_at = models.DateTimeField(null=True)
|
||||
# Project start and target date
|
||||
start_date = models.DateTimeField(null=True, blank=True)
|
||||
target_date = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the project"""
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.db import models
|
||||
|
||||
# Module imports
|
||||
from .base import BaseModel
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
ROLE_CHOICES = (
|
||||
(20, "Owner"),
|
||||
@@ -113,7 +112,19 @@ def get_issue_props():
|
||||
|
||||
|
||||
def slug_validator(value):
|
||||
if value in RESTRICTED_WORKSPACE_SLUGS:
|
||||
if value in [
|
||||
"404",
|
||||
"accounts",
|
||||
"api",
|
||||
"create-workspace",
|
||||
"god-mode",
|
||||
"installations",
|
||||
"invitations",
|
||||
"onboarding",
|
||||
"profile",
|
||||
"spaces",
|
||||
"workspace-invitations",
|
||||
]:
|
||||
raise ValidationError("Slug is not valid")
|
||||
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ from plane.license.utils.instance_value import (
|
||||
get_configuration_value,
|
||||
)
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
|
||||
|
||||
class InstanceEndpoint(BaseAPIView):
|
||||
@@ -38,7 +36,6 @@ class InstanceEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
@cache_response(60 * 60 * 2, user=False)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
def get(self, request):
|
||||
instance = Instance.objects.first()
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ 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__)))
|
||||
|
||||
@@ -128,8 +127,6 @@ 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"
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.db.models import (
|
||||
Sum,
|
||||
Value,
|
||||
When,
|
||||
FloatField,
|
||||
IntegerField,
|
||||
)
|
||||
from django.db.models.functions import (
|
||||
Coalesce,
|
||||
@@ -98,7 +98,7 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
||||
# Estimate
|
||||
else:
|
||||
queryset = queryset.annotate(
|
||||
estimate=Sum(Cast("estimate_point__value", FloatField()))
|
||||
estimate=Sum(Cast("estimate_point__value", IntegerField()))
|
||||
).order_by(x_axis)
|
||||
queryset = (
|
||||
queryset.annotate(segment=F(segment)) if segment else queryset
|
||||
@@ -137,7 +137,7 @@ def burndown_plot(
|
||||
estimate__isnull=False,
|
||||
estimate__type="points",
|
||||
).exists()
|
||||
if estimate_type and plot_type == "points" and cycle_id:
|
||||
if estimate_type and plot_type == "points":
|
||||
issue_estimates = Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -145,18 +145,7 @@ def burndown_plot(
|
||||
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 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]
|
||||
issue_estimates = [int(value) for value in issue_estimates]
|
||||
total_estimate_points = sum(issue_estimates)
|
||||
|
||||
if cycle_id:
|
||||
@@ -238,12 +227,12 @@ def burndown_plot(
|
||||
.order_by("date")
|
||||
)
|
||||
|
||||
if plot_type == "points":
|
||||
for date in date_range:
|
||||
for date in date_range:
|
||||
if plot_type == "points":
|
||||
cumulative_pending_issues = total_estimate_points
|
||||
total_completed = 0
|
||||
total_completed = sum(
|
||||
float(item["estimate_point__value"])
|
||||
int(item["estimate_point__value"])
|
||||
for item in completed_issues_estimate_point_distribution
|
||||
if item["date"] is not None and item["date"] <= date
|
||||
)
|
||||
@@ -252,8 +241,7 @@ def burndown_plot(
|
||||
chart_data[str(date)] = None
|
||||
else:
|
||||
chart_data[str(date)] = cumulative_pending_issues
|
||||
else:
|
||||
for date in date_range:
|
||||
else:
|
||||
cumulative_pending_issues = total_issues
|
||||
total_completed = 0
|
||||
total_completed = sum(
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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.22.0",
|
||||
"version": "0.21.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"private": true,
|
||||
"main": "./index.ts"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -51,6 +51,21 @@ 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);
|
||||
};
|
||||
@@ -59,7 +74,7 @@ export const LinkEditView = ({
|
||||
() => () => {
|
||||
if (linkRemoved.current) return;
|
||||
|
||||
const url = isValidHttpUrl(localUrl) ? localUrl : viewProps.url;
|
||||
const url = isValidUrl(localUrl) ? localUrl : viewProps.url;
|
||||
|
||||
if (to >= editor.state.doc.content.size) return;
|
||||
|
||||
|
||||
@@ -3,4 +3,3 @@ export * from "./embed";
|
||||
export * from "./image";
|
||||
export * from "./mention-suggestion";
|
||||
export * from "./slash-commands-suggestion";
|
||||
export * from "@/plane-editor/types";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"private": true,
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"private": true,
|
||||
"main": "./src/index.d.ts"
|
||||
}
|
||||
|
||||
Vendored
-2
@@ -46,5 +46,3 @@ export type TPageFilters = {
|
||||
sortBy: TPageFiltersSortBy;
|
||||
filters?: TPageFilterProps;
|
||||
};
|
||||
|
||||
export type TPageEmbedType = "mention" | "issue";
|
||||
|
||||
+3
-12
@@ -50,7 +50,6 @@ 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;
|
||||
@@ -64,7 +63,6 @@ export type TNotificationPaginatedInfoQueryParams = {
|
||||
type?: string | undefined;
|
||||
snoozed?: boolean;
|
||||
archived?: boolean;
|
||||
mentioned?: boolean;
|
||||
read?: boolean;
|
||||
per_page?: number;
|
||||
cursor?: string;
|
||||
@@ -86,14 +84,7 @@ export type TNotificationPaginatedInfo = {
|
||||
|
||||
// notification count
|
||||
export type TUnreadNotificationsCount = {
|
||||
total_unread_notifications_count: number;
|
||||
mention_unread_notifications_count: number;
|
||||
};
|
||||
|
||||
export type TNotificationLite = {
|
||||
workspace_slug: string | undefined;
|
||||
project_id: string | undefined;
|
||||
notification_id: string | undefined;
|
||||
issue_id: string | undefined;
|
||||
is_inbox_issue: boolean | undefined;
|
||||
created_issues: number | undefined;
|
||||
my_issues: number | undefined;
|
||||
subscribed_issues: number | undefined;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { fn } from "@storybook/test";
|
||||
import { Avatar } from "./avatar";
|
||||
|
||||
const meta: Meta<typeof Avatar> = {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import { DropdownIcon } from "../icons";
|
||||
import { cn } from "../../helpers";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
hideChevron?: boolean;
|
||||
indicatorElement?: React.ReactNode;
|
||||
actionItemElement?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CollapsibleButton: FC<Props> = (props) => {
|
||||
const { isOpen, title, hideChevron = false, indicatorElement, actionItemElement } = props;
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-100">
|
||||
<div className="flex items-center gap-3.5">
|
||||
<div className="flex items-center gap-3">
|
||||
{!hideChevron && (
|
||||
<DropdownIcon
|
||||
className={cn("size-2 text-custom-text-300 hover:text-custom-text-200 duration-300", {
|
||||
"-rotate-90": !isOpen,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<span className="text-base text-custom-text-100 font-medium">{title}</span>
|
||||
</div>
|
||||
{indicatorElement && indicatorElement}
|
||||
</div>
|
||||
{actionItemElement && isOpen && actionItemElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./collapsible";
|
||||
export * from "./collapsible-button";
|
||||
@@ -34,7 +34,6 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
onMenuClose,
|
||||
tabIndex,
|
||||
closeOnSelect,
|
||||
openOnHover = false,
|
||||
} = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
|
||||
@@ -69,24 +68,12 @@ 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);
|
||||
|
||||
@@ -124,8 +111,6 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
className={cn("relative w-min text-left", className)}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
onClick={handleOnClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
@@ -218,4 +203,4 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||
|
||||
CustomMenu.MenuItem = MenuItem;
|
||||
|
||||
export { CustomMenu };
|
||||
export { CustomMenu };
|
||||
|
||||
@@ -28,7 +28,6 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
|
||||
onMenuClose?: () => void;
|
||||
closeOnSelect?: boolean;
|
||||
portalElement?: Element | null;
|
||||
openOnHover?: boolean;
|
||||
}
|
||||
|
||||
export interface ICustomSelectProps extends IDropdownProps {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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,4 +21,3 @@ export * from "./related-icon";
|
||||
export * from "./side-panel-icon";
|
||||
export * from "./transfer-icon";
|
||||
export * from "./info-icon";
|
||||
export * from "./dropdown-icon";
|
||||
|
||||
@@ -20,5 +20,3 @@ export * from "./drag-handle";
|
||||
export * from "./drop-indicator";
|
||||
export * from "./favorite-star";
|
||||
export * from "./loader";
|
||||
export * from "./collapsible";
|
||||
export * from "./popovers";
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./popover";
|
||||
export * from "./popover-menu";
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { PopoverMenu } from "./popover-menu";
|
||||
|
||||
const meta: Meta<typeof PopoverMenu> = {
|
||||
title: "PopoverMenu",
|
||||
component: PopoverMenu,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
// types
|
||||
type TPopoverMenu = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof PopoverMenu<TPopoverMenu>>;
|
||||
|
||||
// data
|
||||
const data: TPopoverMenu[] = [
|
||||
{ id: 1, name: "John Doe" },
|
||||
{ id: 2, name: "Jane Doe" },
|
||||
{ id: 3, name: "John Smith" },
|
||||
{ id: 4, name: "Jane Smith" },
|
||||
];
|
||||
|
||||
// components
|
||||
const PopoverMenuItemRender = (item: TPopoverMenu) => (
|
||||
<div className="text-sm text-gray-600 hover:text-gray-700 rounded-sm cursor-pointer hover:bg-gray-200 transition-all px-1.5 py-0.5 capitalize">
|
||||
{item.name}
|
||||
</div>
|
||||
);
|
||||
|
||||
// stories
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
popperPosition: "bottom-start",
|
||||
panelClassName: "rounded bg-gray-100 p-2",
|
||||
data: data,
|
||||
keyExtractor: (item, index: number) => `${item.id}-${index}`,
|
||||
render: (item) => PopoverMenuItemRender(item),
|
||||
},
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import React, { Fragment } from "react";
|
||||
// components
|
||||
import { Popover } from "./popover";
|
||||
// helpers
|
||||
import { cn } from "../../helpers";
|
||||
// types
|
||||
import { TPopoverMenu } from "./types";
|
||||
|
||||
export const PopoverMenu = <T,>(props: TPopoverMenu<T>) => {
|
||||
const {
|
||||
popperPosition = "bottom-end",
|
||||
popperPadding = 0,
|
||||
buttonClassName = "",
|
||||
button,
|
||||
panelClassName = "",
|
||||
data,
|
||||
keyExtractor,
|
||||
render,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
popperPosition={popperPosition}
|
||||
popperPadding={popperPadding}
|
||||
buttonClassName={buttonClassName}
|
||||
button={button}
|
||||
panelClassName={cn(
|
||||
"my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2 text-xs shadow-custom-shadow-rg focus:outline-none",
|
||||
panelClassName
|
||||
)}
|
||||
>
|
||||
<Fragment>
|
||||
{data.map((item, index) => (
|
||||
<Fragment key={keyExtractor(item, index)}>{render(item, index)}</Fragment>
|
||||
))}
|
||||
</Fragment>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { Popover } from "./popover";
|
||||
|
||||
const meta: Meta<typeof Popover> = {
|
||||
title: "Popover",
|
||||
component: Popover,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
// types
|
||||
type Story = StoryObj<typeof Popover>;
|
||||
|
||||
// data
|
||||
|
||||
// components
|
||||
const RenderCustomPopoverComponent = (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-gray-500">Your custom component</div>
|
||||
<div>
|
||||
{["option1", "option2", "option3"].map((option) => (
|
||||
<div
|
||||
key={option}
|
||||
className="text-sm text-gray-600 hover:text-gray-700 rounded-sm cursor-pointer hover:bg-gray-200 transition-all px-1.5 py-0.5 capitalize"
|
||||
>
|
||||
{option}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// stories
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
popperPosition: "bottom-start",
|
||||
panelClassName: "rounded bg-gray-100 p-2",
|
||||
children: RenderCustomPopoverComponent,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomMenuButton: Story = {
|
||||
args: {
|
||||
popperPosition: "bottom-start",
|
||||
button: (
|
||||
<div className="p-2 text-sm font-medium rounded bg-gray-100 hover:bg-gray-200 transition-all">
|
||||
Custom Menu Button
|
||||
</div>
|
||||
),
|
||||
panelClassName: "rounded bg-gray-100 p-2",
|
||||
children: RenderCustomPopoverComponent,
|
||||
},
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Popover as HeadlessReactPopover, Transition } from "@headlessui/react";
|
||||
// helpers
|
||||
import { cn } from "../../helpers";
|
||||
// types
|
||||
import { TPopover } from "./types";
|
||||
import { EllipsisVertical } from "lucide-react";
|
||||
|
||||
export const Popover = (props: TPopover) => {
|
||||
const {
|
||||
popperPosition = "bottom-end",
|
||||
popperPadding = 0,
|
||||
buttonClassName = "",
|
||||
button,
|
||||
panelClassName = "",
|
||||
children,
|
||||
} = props;
|
||||
// states
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
// react-popper derived values
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: popperPosition,
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: popperPadding,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<HeadlessReactPopover className="relative flex h-full w-full items-center justify-center">
|
||||
<HeadlessReactPopover.Button ref={setReferenceElement} className="flex justify-center items-center">
|
||||
{button ? (
|
||||
button
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-center items-center text-base h-6 w-6 rounded transition-all bg-custom-background-90 hover:bg-custom-background-80",
|
||||
buttonClassName
|
||||
)}
|
||||
>
|
||||
<EllipsisVertical className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
</HeadlessReactPopover.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<HeadlessReactPopover.Panel
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
className={cn("absolute left-0 top-full z-20 w-screen max-w-xs mt-2", panelClassName)}
|
||||
>
|
||||
{children}
|
||||
</HeadlessReactPopover.Panel>
|
||||
</Transition>
|
||||
</HeadlessReactPopover>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
|
||||
export type TPopoverButtonDefaultOptions = {
|
||||
// button and button styling
|
||||
button?: ReactNode;
|
||||
buttonClassName?: string;
|
||||
};
|
||||
|
||||
export type TPopoverDefaultOptions = TPopoverButtonDefaultOptions & {
|
||||
// popper styling
|
||||
popperPosition?: Placement | undefined;
|
||||
popperPadding?: number | undefined;
|
||||
// panel styling
|
||||
panelClassName?: string;
|
||||
};
|
||||
|
||||
export type TPopover = TPopoverDefaultOptions & {
|
||||
// children
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export type TPopoverMenu<T> = TPopoverDefaultOptions & {
|
||||
data: T[];
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
render: (item: T, index: number) => ReactNode;
|
||||
};
|
||||
@@ -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-[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">
|
||||
<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">
|
||||
<div className="space-y-1 p-2">
|
||||
{PEEK_MODES.map((mode) => (
|
||||
<Listbox.Option
|
||||
@@ -95,14 +95,24 @@ 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 ? "bg-custom-background-80" : ""
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
|
||||
{mode.label}
|
||||
</div>
|
||||
{({ 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>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
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();
|
||||
@@ -16,7 +15,7 @@ const WorkspaceActiveCyclesPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<WorkspaceActiveCyclesRoot />
|
||||
<WorkspaceActiveCyclesUpgrade />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,9 @@ import { NotificationsSidebar } from "@/components/workspace-notifications";
|
||||
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="relative w-full h-full overflow-hidden flex items-center">
|
||||
<NotificationsSidebar />
|
||||
<div className="relative w-full lg:w-2/6 border-0 lg:border-r border-custom-border-200 z-[10] flex-shrink-0 bg-custom-background-100 h-full transition-all overflow-hidden">
|
||||
<NotificationsSidebar />
|
||||
</div>
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,32 +3,19 @@
|
||||
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 { useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
|
||||
const WorkspaceDashboardPage = observer(() => {
|
||||
// hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const {
|
||||
currentSelectedNotificationId,
|
||||
setCurrentSelectedNotificationId,
|
||||
notificationLiteByNotificationId,
|
||||
notificationIdsByWorkspaceId,
|
||||
getNotifications,
|
||||
} = useWorkspaceNotifications();
|
||||
const {
|
||||
membership: { fetchUserProjectInfo },
|
||||
} = useUser();
|
||||
const { notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined;
|
||||
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
|
||||
notificationLiteByNotificationId(currentSelectedNotificationId);
|
||||
|
||||
// fetch workspace notifications
|
||||
const notificationMutation =
|
||||
@@ -42,46 +29,15 @@ const WorkspaceDashboardPage = observer(() => {
|
||||
useSWR(
|
||||
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null,
|
||||
currentWorkspace?.slug
|
||||
? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
|
||||
? async () => 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">
|
||||
{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
|
||||
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<IssuePeekOverview
|
||||
embedIssue
|
||||
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
|
||||
/>
|
||||
)}
|
||||
<IssuePeekOverview embedIssue />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
ISSUE_LAYOUTS,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel } from "@/hooks/store";
|
||||
|
||||
@@ -108,6 +108,8 @@ 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
|
||||
@@ -144,7 +146,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
|
||||
+3
-6
@@ -24,7 +24,7 @@ import {
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
@@ -152,6 +152,7 @@ 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 (
|
||||
@@ -247,11 +248,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
+5
-9
@@ -12,15 +12,9 @@ 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 { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
|
||||
|
||||
@@ -115,6 +109,8 @@ export const CycleIssuesMobileHeader = () => {
|
||||
[workspaceSlug, projectId, cycleId, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -154,7 +150,7 @@ export const CycleIssuesMobileHeader = () => {
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
+1
-1
@@ -98,4 +98,4 @@ const ProjectCyclesPage = observer(() => {
|
||||
);
|
||||
});
|
||||
|
||||
export default ProjectCyclesPage;
|
||||
export default ProjectCyclesPage;
|
||||
+4
-2
@@ -18,7 +18,7 @@ import {
|
||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
@@ -90,6 +90,8 @@ 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">
|
||||
@@ -138,7 +140,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
+6
-5
@@ -22,8 +22,8 @@ import {
|
||||
} from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { SPACE_BASE_URL } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import {
|
||||
useEventTracker,
|
||||
@@ -107,12 +107,13 @@ export const ProjectIssuesHeader = observer(() => {
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
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 publishedURL = `${SPACE_BASE_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 (
|
||||
@@ -192,7 +193,7 @@ export const ProjectIssuesHeader = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
+5
-9
@@ -13,15 +13,9 @@ 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 { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
@@ -94,6 +88,8 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -133,7 +129,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
+3
-6
@@ -24,7 +24,7 @@ import {
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
@@ -152,6 +152,7 @@ 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 (
|
||||
@@ -248,11 +249,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
+5
-9
@@ -13,15 +13,9 @@ 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 { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
@@ -96,6 +90,8 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="block md:hidden">
|
||||
<ProjectAnalyticsModal
|
||||
@@ -135,7 +131,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props) => {
|
||||
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
|
||||
<ProjectSettingsSidebar />
|
||||
</div>
|
||||
<div className="w-full pl-10 sm:pl-10 md:pl-3 lg:pl-3 overflow-y-scroll vertical-scrollbar scrollbar-md">
|
||||
<div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0 overflow-x-hidden overflow-y-scroll vertical-scrollbar scrollbar-md">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-8
@@ -1,30 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectStateRoot } from "@/components/project-states";
|
||||
import { ProjectSettingStateList } from "@/components/states";
|
||||
// hook
|
||||
import { useProject } from "@/hooks/store";
|
||||
|
||||
const StatesSettingsPage = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="py-8 pr-9">
|
||||
<div className="w-full gap-10 overflow-y-auto py-8 pr-9">
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">States</h3>
|
||||
</div>
|
||||
{workspaceSlug && projectId && (
|
||||
<ProjectStateRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
)}
|
||||
<ProjectSettingStateList />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
+4
-2
@@ -22,7 +22,7 @@ import {
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
@@ -133,6 +133,8 @@ 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">
|
||||
@@ -231,7 +233,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
disabled={!canUserCreateIssue}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<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 isDefaultView={!!defaultView} />
|
||||
<AllIssueLayoutRoot />
|
||||
</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 { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useLabel, useMember, useUser, useIssues, useGlobalView } from "@/hooks/store";
|
||||
|
||||
@@ -98,6 +98,7 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
const isLocked = viewDetails?.is_locked;
|
||||
|
||||
return (
|
||||
@@ -117,11 +118,7 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
<div className="flex items-center gap-2">
|
||||
{!isLocked && (
|
||||
<>
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
@@ -5,8 +5,6 @@ import "@/styles/globals.css";
|
||||
import "@/styles/command-pallette.css";
|
||||
import "@/styles/emoji.css";
|
||||
import "@/styles/react-day-picker.css";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// local
|
||||
import { AppProvider } from "./provider";
|
||||
|
||||
@@ -38,17 +36,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest.json" />
|
||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||
{/* preloading */}
|
||||
<link rel="preload" href={`${API_BASE_URL}/api/instances/`} as="fetch" crossOrigin="use-credentials" />
|
||||
<link rel="preload" href={`${API_BASE_URL}/api/users/me/ `} as="fetch" crossOrigin="use-credentials" />
|
||||
<link rel="preload" href={`${API_BASE_URL}/api/users/me/profile/ `} as="fetch" crossOrigin="use-credentials" />
|
||||
<link rel="preload" href={`${API_BASE_URL}/api/users/me/settings/ `} as="fetch" crossOrigin="use-credentials" />
|
||||
<link
|
||||
rel="preload"
|
||||
href={`${API_BASE_URL}/api/users/me/workspaces/`}
|
||||
as="fetch"
|
||||
crossOrigin="use-credentials"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="context-menu-portal" />
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./root";
|
||||
export * from "./workspace-active-cycles-upgrade";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user