Compare commits

..

2 Commits

Author SHA1 Message Date
rahulramesha f04220818c override methods used for typeError 2024-07-03 18:49:49 +05:30
rahulramesha 98562a55c7 fix issue changes done in Peek overview to reflect in the issue boards 2024-07-02 15:48:54 +05:30
128 changed files with 640 additions and 1459 deletions
+13 -18
View File
@@ -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 }}
+1 -7
View File
@@ -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]
@@ -56,9 +52,7 @@ jobs:
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
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
+1 -11
View File
@@ -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
View File
@@ -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

-11
View File
@@ -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

+15 -16
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.22.0"
"version": "0.21.0"
}
+7 -7
View File
@@ -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,
+1 -1
View File
@@ -149,7 +149,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
description_html=request.data.get("issue", {}).get(
"description_html", "<p></p>"
),
priority=request.data.get("issue", {}).get("priority", "none"),
priority=request.data.get("issue", {}).get("priority", "low"),
project_id=project_id,
state=state,
)
+6 -6
View File
@@ -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
+16 -6
View File
@@ -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
+31 -31
View File
@@ -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,
@@ -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,
@@ -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
+18 -19
View File
@@ -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,
+31 -12
View File
@@ -47,18 +47,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
type = request.GET.get("type", "all")
q_filters = Q()
inbox_issue = Issue.objects.filter(
pk=OuterRef("entity_identifier"),
issue_inbox__status__in=[0, 2, -2],
workspace__slug=self.kwargs.get("slug"),
)
notifications = (
Notification.objects.filter(
workspace__slug=slug, receiver_id=request.user.id
)
.filter(entity_name="issue")
.annotate(is_inbox_issue=Exists(inbox_issue))
.select_related("workspace", "project", "triggered_by", "receiver")
.order_by("snoozed_till", "-created_at")
)
@@ -210,19 +202,46 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
class UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug):
# Watching Issues Count
unread_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,
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
)
"subscribed_issues": subscribed_issues_count,
"my_issues": my_issues_count,
"created_issues": created_issues_count,
},
status=status.HTTP_200_OK,
)
+2 -6
View File
@@ -44,7 +44,6 @@ from plane.db.models import (
WorkspaceTheme,
)
from plane.utils.cache import cache_response, invalidate_cache
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
class WorkSpaceViewSet(BaseViewSet):
@@ -119,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(
@@ -232,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)
+10 -12
View File
@@ -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),
-1
View File
@@ -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)
+13 -2
View File
@@ -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")
-3
View File
@@ -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"
+6 -18
View File
@@ -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(
-30
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.22.0",
"version": "0.21.0",
"private": true,
"main": "./index.ts"
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tsconfig",
"version": "0.22.0",
"version": "0.21.0",
"private": true,
"files": [
"base.json",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.22.0",
"version": "0.21.0",
"private": true,
"main": "./src/index.d.ts"
}
+3 -10
View File
@@ -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;
@@ -85,13 +84,7 @@ export type TNotificationPaginatedInfo = {
// notification count
export type TUnreadNotificationsCount = {
total_unread_notifications_count: number;
};
export type TCurrentSelectedNotification = {
workspace_slug: string | undefined;
project_id: string | undefined;
notification_id: string | undefined;
issue_id: string | undefined;
is_inbox_issue: boolean | undefined;
created_issues: number | undefined;
my_issues: number | undefined;
subscribed_issues: number | undefined;
};
+1 -1
View File
@@ -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,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
View File
@@ -1 +0,0 @@
export * from "./collapsible";
+4 -19
View File
@@ -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 };
-1
View File
@@ -28,7 +28,6 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
onMenuClose?: () => void;
closeOnSelect?: boolean;
portalElement?: Element | null;
openOnHover?: boolean;
}
export interface ICustomSelectProps extends IDropdownProps {
-15
View File
@@ -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>
);
-2
View File
@@ -21,5 +21,3 @@ export * from "./related-icon";
export * from "./side-panel-icon";
export * from "./transfer-icon";
export * from "./info-icon";
export * from "./relations-icon";
export * from "./dropdown-icon";
-25
View File
@@ -1,25 +0,0 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const RelationsIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg
viewBox="0 0 16 16"
className={`${className}`}
stroke="currentColor"
fill="none"
xmlns="http://www.w3.org/2000/svg"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
{...rest}
>
<path d="M7.99998 4.66536C8.92045 4.66536 9.66665 3.91917 9.66665 2.9987C9.66665 2.07822 8.92045 1.33203 7.99998 1.33203C7.07951 1.33203 6.33331 2.07822 6.33331 2.9987C6.33331 3.91917 7.07951 4.66536 7.99998 4.66536Z" />
<path d="M6.80001 4.19922L4.20001 6.79922" />
<path d="M2.99998 9.66536C3.92045 9.66536 4.66665 8.91917 4.66665 7.9987C4.66665 7.07822 3.92045 6.33203 2.99998 6.33203C2.07951 6.33203 1.33331 7.07822 1.33331 7.9987C1.33331 8.91917 2.07951 9.66536 2.99998 9.66536Z" />
<path d="M4.66669 8H11.3334" />
<path d="M13 9.66536C13.9205 9.66536 14.6666 8.91917 14.6666 7.9987C14.6666 7.07822 13.9205 6.33203 13 6.33203C12.0795 6.33203 11.3333 7.07822 11.3333 7.9987C11.3333 8.91917 12.0795 9.66536 13 9.66536Z" />
<path d="M9.20001 11.7992L11.8 9.19922" />
<path d="M7.99998 14.6654C8.92045 14.6654 9.66665 13.9192 9.66665 12.9987C9.66665 12.0782 8.92045 11.332 7.99998 11.332C7.07951 11.332 6.33331 12.0782 6.33331 12.9987C6.33331 13.9192 7.07951 14.6654 7.99998 14.6654Z" />
</svg>
);
-1
View File
@@ -20,4 +20,3 @@ export * from "./drag-handle";
export * from "./drop-indicator";
export * from "./favorite-star";
export * from "./loader";
export * from "./collapsible";
+1 -1
View File
@@ -17,7 +17,7 @@ export const ModalCore: React.FC<Props> = (props) => {
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose && handleClose()}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose && handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -87,7 +87,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options className="absolute left-0 z-10 mt-1 min-w-[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
View File
@@ -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 />
</>
);
});
@@ -3,25 +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 { currentSelectedNotification, 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 } = currentSelectedNotification;
// fetch workspace notifications
const notificationMutation =
@@ -35,42 +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
/>
)}
</>
) : (
<IssuePeekOverview embedIssue />
)}
<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={
@@ -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}
@@ -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 ?? {}}
@@ -98,4 +98,4 @@ const ProjectCyclesPage = observer(() => {
);
});
export default ProjectCyclesPage;
export default ProjectCyclesPage;
@@ -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}
@@ -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}
@@ -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 ?? {}}
@@ -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}
@@ -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 ?? {}}
@@ -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 ?? {}}
-2
View File
@@ -1,2 +0,0 @@
export * from "./root";
export * from "./workspace-active-cycles-upgrade";
-3
View File
@@ -1,3 +0,0 @@
import { WorkspaceActiveCyclesUpgrade } from "@/plane-web/components/active-cycles";
export const WorkspaceActiveCyclesRoot = () => <WorkspaceActiveCyclesUpgrade />;
@@ -139,6 +139,11 @@ export const CommandPalette: FC = observer(() => {
description: "Create a new issue in the current project",
action: () => toggleCreateIssueModal(true),
},
h: {
title: "Show shortcuts",
description: "Show all the available shortcuts",
action: () => toggleShortcutModal(true),
},
},
workspace: {
p: {
@@ -194,18 +199,16 @@ export const CommandPalette: FC = observer(() => {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
const { key, ctrlKey, metaKey, altKey } = e;
if (!key) return;
const keyPressed = key.toLowerCase();
const cmdClicked = ctrlKey || metaKey;
const shiftClicked = shiftKey;
if (cmdClicked && keyPressed === "k" && !isAnyModalOpen) {
e.preventDefault();
toggleCommandPaletteModal(true);
}
// if on input, textarea or editor, don't do anything
if (
e.target instanceof HTMLTextAreaElement ||
@@ -214,11 +217,6 @@ export const CommandPalette: FC = observer(() => {
)
return;
if (shiftClicked && (keyPressed === "?" || keyPressed === "/") && !isAnyModalOpen) {
e.preventDefault();
toggleShortcutModal(true);
}
if (cmdClicked) {
if (keyPressed === "c" && ((platform === "MacOS" && ctrlKey) || altKey)) {
e.preventDefault();
@@ -29,7 +29,7 @@ export const ShortcutCommandsList: React.FC<Props> = (props) => {
{ keys: "V", description: "Create view" },
{ keys: "D", description: "Create page" },
{ keys: "Delete", description: "Bulk delete issues" },
{ keys: "Shift,/", description: "Open shortcuts guide" },
{ keys: "H", description: "Open shortcuts guide" },
{
keys: platform === "MacOS" ? "Ctrl,control,C" : "Ctrl,Alt,C",
description: "Copy issue URL from the issue details page",
-11
View File
@@ -1,7 +1,6 @@
"use client";
import React, { useEffect, useState } from "react";
import { mutate } from "swr";
// types
import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types";
// ui
@@ -44,16 +43,6 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const selectedProjectId = payload.project_id ?? projectId.toString();
await createCycle(workspaceSlug, selectedProjectId, payload)
.then((res) => {
// mutate when the current cycle creation is active
if (payload.start_date && payload.end_date) {
const currentDate = new Date();
const cycleStartDate = new Date(payload.start_date);
const cycleEndDate = new Date(payload.end_date);
if (currentDate >= cycleStartDate && currentDate <= cycleEndDate) {
mutate(`PROJECT_ACTIVE_CYCLE_${selectedProjectId}`);
}
}
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
+1 -8
View File
@@ -236,14 +236,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
<Combobox.Option key={option.value} value={option.value}>
{({ active, selected }) => (
<div
className={cn(
"flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
{
"bg-custom-background-80": active,
"text-custom-text-100": selected,
"text-custom-text-200": !selected,
}
)}
className={`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${active ? `!hover:bg-custom-background-80` : ``} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`}
>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
@@ -39,7 +39,6 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
showAccessSpecifier = false,
showSubmitButton = true,
isSubmitting = false,
placeholder = "Add comment...",
...rest
} = props;
// store hooks
@@ -82,9 +81,8 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
highlights: mentionHighlights,
suggestions: mentionSuggestions,
}}
placeholder={placeholder}
containerClassName={cn(containerClassName, "relative")}
{...rest}
containerClassName={cn(containerClassName, "relative")}
/>
<IssueCommentToolbar
accessSpecifier={accessSpecifier}
@@ -54,7 +54,7 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
ganttContainerRef,
isPaginating ? null : intersectionElement,
loadMoreBlocks,
"100% 0% 100% 0%"
"50% 0% 50% 0%"
);
const handleOnDrop = (
@@ -16,44 +16,44 @@ import {
VideoIcon,
} from "@/components/icons/attachment";
export const getFileIcon = (fileType: string, size: number = 28) => {
export const getFileIcon = (fileType: string) => {
switch (fileType) {
case "pdf":
return <PdfIcon height={size} width={size} />;
return <PdfIcon height={28} width={28} />;
case "csv":
return <CsvIcon height={size} width={size} />;
return <CsvIcon height={28} width={28} />;
case "xlsx":
return <SheetIcon height={size} width={size} />;
return <SheetIcon height={28} width={28} />;
case "css":
return <CssIcon height={size} width={size} />;
return <CssIcon height={28} width={28} />;
case "doc":
return <DocIcon height={size} width={size} />;
return <DocIcon height={28} width={28} />;
case "fig":
return <FigmaIcon height={size} width={size} />;
return <FigmaIcon height={28} width={28} />;
case "html":
return <HtmlIcon height={size} width={size} />;
return <HtmlIcon height={28} width={28} />;
case "png":
return <PngIcon height={size} width={size} />;
return <PngIcon height={28} width={28} />;
case "jpg":
return <JpgIcon height={size} width={size} />;
return <JpgIcon height={28} width={28} />;
case "js":
return <JavaScriptIcon height={size} width={size} />;
return <JavaScriptIcon height={28} width={28} />;
case "txt":
return <TxtIcon height={size} width={size} />;
return <TxtIcon height={28} width={28} />;
case "svg":
return <SvgIcon height={size} width={size} />;
return <SvgIcon height={28} width={28} />;
case "mp3":
return <AudioIcon height={size} width={size} />;
return <AudioIcon height={28} width={28} />;
case "wav":
return <AudioIcon height={size} width={size} />;
return <AudioIcon height={28} width={28} />;
case "mp4":
return <VideoIcon height={size} width={size} />;
return <VideoIcon height={28} width={28} />;
case "wmv":
return <VideoIcon height={size} width={size} />;
return <VideoIcon height={28} width={28} />;
case "mkv":
return <VideoIcon height={size} width={size} />;
return <VideoIcon height={28} width={28} />;
default:
return <DefaultIcon height={size} width={size} />;
return <DefaultIcon height={28} width={28} />;
}
};
@@ -44,19 +44,10 @@ type TInboxIssueActionsHeader = {
isSubmitting: "submitting" | "submitted" | "saved";
isMobileSidebar: boolean;
setIsMobileSidebar: (value: boolean) => void;
isNotificationEmbed: boolean;
};
export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((props) => {
const {
workspaceSlug,
projectId,
inboxIssue,
isSubmitting,
isMobileSidebar,
setIsMobileSidebar,
isNotificationEmbed = false,
} = props;
const { workspaceSlug, projectId, inboxIssue, isSubmitting, isMobileSidebar, setIsMobileSidebar } = props;
// states
const [isSnoozeDateModalOpen, setIsSnoozeDateModalOpen] = useState(false);
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
@@ -67,7 +58,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const { currentTab, deleteInboxIssue, filteredInboxIssueIds } = useProjectInbox();
const { data: currentUser } = useUser();
const {
membership: { currentProjectRoleByProjectId },
membership: { currentProjectRole },
} = useUser();
const router = useAppRouter();
@@ -75,7 +66,6 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const issue = inboxIssue?.issue;
// derived values
const currentProjectRole = currentProjectRoleByProjectId(projectId) || undefined;
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const canMarkAsDuplicate = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
const canMarkAsAccepted = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
@@ -101,13 +91,11 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
};
const handleRedirection = (nextOrPreviousIssueId: string | undefined) => {
if (!isNotificationEmbed) {
if (nextOrPreviousIssueId)
router.push(
`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}&inboxIssueId=${nextOrPreviousIssueId}`
);
else router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
}
if (nextOrPreviousIssueId)
router.push(
`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}&inboxIssueId=${nextOrPreviousIssueId}`
);
else router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
};
const handleInboxIssueAccept = async () => {
@@ -138,7 +126,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const handleInboxIssueDelete = async () => {
if (!inboxIssue || !currentInboxIssueId) return;
await deleteInboxIssue(workspaceSlug, projectId, currentInboxIssueId).finally(() => {
if (!isNotificationEmbed) router.push(`/${workspaceSlug}/projects/${projectId}/inbox`);
router.push(`/${workspaceSlug}/projects/${projectId}/inbox`);
});
};
@@ -191,11 +179,11 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
);
useEffect(() => {
if (!isNotificationEmbed) document.addEventListener("keydown", onKeyDown);
document.addEventListener("keydown", onKeyDown);
return () => {
if (!isNotificationEmbed) document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("keydown", onKeyDown);
};
}, [onKeyDown, isNotificationEmbed]);
}, [onKeyDown]);
if (!inboxIssue) return null;
@@ -252,24 +240,22 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
</div>
<div className="flex items-center gap-2">
{!isNotificationEmbed && (
<div className="flex items-center gap-x-2">
<button
type="button"
className="rounded border border-custom-border-200 p-1.5"
onClick={() => handleInboxIssueNavigation("prev")}
>
<ChevronUp size={14} strokeWidth={2} />
</button>
<button
type="button"
className="rounded border border-custom-border-200 p-1.5"
onClick={() => handleInboxIssueNavigation("next")}
>
<ChevronDown size={14} strokeWidth={2} />
</button>
</div>
)}
<div className="flex items-center gap-x-2">
<button
type="button"
className="rounded border border-custom-border-200 p-1.5"
onClick={() => handleInboxIssueNavigation("prev")}
>
<ChevronUp size={14} strokeWidth={2} />
</button>
<button
type="button"
className="rounded border border-custom-border-200 p-1.5"
onClick={() => handleInboxIssueNavigation("next")}
>
<ChevronDown size={14} strokeWidth={2} />
</button>
</div>
<div className="flex flex-wrap items-center gap-2">
{canMarkAsAccepted && (
@@ -327,12 +313,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
{isAllowed && (
<CustomMenu verticalEllipsis placement="bottom-start">
{canMarkAsAccepted && (
<CustomMenu.MenuItem onClick={handleIssueSnoozeAction}>
<CustomMenu.MenuItem onClick={handleIssueSnoozeAction}>
<div className="flex items-center gap-2">
<Clock size={14} strokeWidth={2} />
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0
? "Un-snooze"
: "Snooze"}
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0
? "Un-snooze"
: "Snooze"}
</div>
</CustomMenu.MenuItem>
)}
+3 -12
View File
@@ -15,18 +15,10 @@ type TInboxContentRoot = {
inboxIssueId: string;
isMobileSidebar: boolean;
setIsMobileSidebar: (value: boolean) => void;
isNotificationEmbed?: boolean;
};
export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
const {
workspaceSlug,
projectId,
inboxIssueId,
isMobileSidebar,
setIsMobileSidebar,
isNotificationEmbed = false,
} = props;
const { workspaceSlug, projectId, inboxIssueId, isMobileSidebar, setIsMobileSidebar } = props;
/// router
const router = useAppRouter();
// states
@@ -41,11 +33,11 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
useEffect(() => {
if (!isIssueAvailable && inboxIssueId && !isNotificationEmbed) {
if (!isIssueAvailable && inboxIssueId) {
router.replace(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isIssueAvailable, isNotificationEmbed]);
}, [isIssueAvailable]);
useSWR(
workspaceSlug && projectId && inboxIssueId
@@ -77,7 +69,6 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
projectId={projectId}
inboxIssue={inboxIssue}
isSubmitting={isSubmitting}
isNotificationEmbed={isNotificationEmbed || false}
/>
</div>
<div className="h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto px-6 py-5 vertical-scrollbar scrollbar-md">
@@ -9,7 +9,7 @@ import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/com
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
@@ -63,6 +63,8 @@ export const ArchivedIssuesHeader: FC = observer(() => {
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property);
};
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
return (
<div className="group relative flex border-b border-custom-border-200">
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
@@ -70,7 +72,7 @@ export const ArchivedIssuesHeader: FC = observer(() => {
</div>
{/* filter options */}
<div className="flex items-center gap-2 px-8">
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
<FilterSelection
filters={issueFilters?.filters || {}}
handleFiltersUpdate={handleFiltersUpdate}
@@ -1,6 +1,6 @@
"use client";
import { FC, useState } from "react";
import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { AlertCircle, X } from "lucide-react";
@@ -35,9 +35,9 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
const { getUserDetails } = useMember();
const {
attachment: { getAttachmentById },
isDeleteAttachmentModalOpen,
toggleDeleteAttachmentModal,
} = useIssueDetail();
// state
const [isDeleteIssueAttachmentModalOpen, setIsDeleteIssueAttachmentModalOpen] = useState(false);
// derived values
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
// hooks
@@ -47,10 +47,10 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
return (
<>
{isDeleteIssueAttachmentModalOpen && (
{isDeleteAttachmentModalOpen === attachment.id && (
<IssueAttachmentDeleteModal
isOpen={isDeleteIssueAttachmentModalOpen}
onClose={() => setIsDeleteIssueAttachmentModalOpen(false)}
isOpen={!!isDeleteAttachmentModalOpen}
onClose={() => toggleDeleteAttachmentModal(null)}
handleAttachmentOperations={handleAttachmentOperations}
data={attachment}
/>
@@ -58,7 +58,7 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
<div className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-custom-border-200 bg-custom-background-100 px-4 py-2 text-sm">
<Link href={attachment.asset} target="_blank" rel="noopener noreferrer">
<div className="flex items-center gap-3">
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset), 28)}</div>
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Tooltip tooltipContent={getFileName(attachment.attributes.name)} isMobile={isMobile}>
@@ -85,7 +85,7 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
</Link>
{!disabled && (
<button type="button" onClick={() => setIsDeleteIssueAttachmentModalOpen(true)}>
<button type="button" onClick={() => toggleDeleteAttachmentModal(attachment.id)}>
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
</button>
)}
@@ -1,93 +0,0 @@
import { FC, useCallback, useState } from "react";
import { observer } from "mobx-react";
import { useDropzone } from "react-dropzone";
import { UploadCloud } from "lucide-react";
// hooks
import { MAX_FILE_SIZE } from "@/constants/common";
import { generateFileName } from "@/helpers/attachment.helper";
import { useInstance, useIssueDetail } from "@/hooks/store";
// components
import { IssueAttachmentsListItem } from "./attachment-list-item";
// types
import { TAttachmentOperations } from "./root";
type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
type TIssueAttachmentItemList = {
workspaceSlug: string;
issueId: string;
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
disabled?: boolean;
};
export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((props) => {
const { workspaceSlug, issueId, handleAttachmentOperations, disabled } = props;
const [isLoading, setIsLoading] = useState(false);
// store hooks
const { config } = useInstance();
const {
attachment: { getAttachmentsByIssueId },
} = useIssueDetail();
// derived values
const issueAttachments = getAttachmentsByIssueId(issueId);
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const currentFile: File = acceptedFiles[0];
if (!currentFile || !workspaceSlug) return;
const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), {
type: currentFile.type,
});
const formData = new FormData();
formData.append("asset", uploadedFile);
formData.append(
"attributes",
JSON.stringify({
name: uploadedFile.name,
size: uploadedFile.size,
})
);
setIsLoading(true);
handleAttachmentOperations.create(formData).finally(() => setIsLoading(false));
},
[handleAttachmentOperations, workspaceSlug]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
maxSize: config?.file_size_limit ?? MAX_FILE_SIZE,
multiple: false,
disabled: isLoading || disabled,
});
if (!issueAttachments) return <></>;
return (
<div
{...getRootProps()}
className={`relative flex flex-col ${isDragActive && issueAttachments.length < 3 ? "min-h-[200px]" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
>
<input {...getInputProps()} />
{isDragActive && (
<div className="absolute flex items-center justify-center left-0 top-0 h-full w-full bg-custom-background-90/75 z-30 ">
<div className="flex items-center justify-center p-1 rounded-md bg-custom-background-100">
<div className="flex flex-col justify-center items-center px-5 py-6 rounded-md border border-dashed border-custom-border-300">
<UploadCloud className="size-7" />
<span className="text-sm text-custom-text-300">Drag and drop anywhere to upload</span>
</div>
</div>
</div>
)}
{issueAttachments?.map((attachmentId) => (
<IssueAttachmentsListItem
key={attachmentId}
attachmentId={attachmentId}
disabled={disabled}
handleAttachmentOperations={handleAttachmentOperations}
/>
))}
</div>
);
});
@@ -1,112 +0,0 @@
"use client";
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { Trash } from "lucide-react";
// ui
import { CustomMenu, Tooltip } from "@plane/ui";
// components
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { getFileIcon } from "@/components/icons";
import { IssueAttachmentDeleteModal } from "@/components/issues";
// helpers
import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper";
import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { useIssueDetail, useMember } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
import { TAttachmentOperations } from "./root";
type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
type TIssueAttachmentsListItem = {
attachmentId: string;
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
disabled?: boolean;
};
export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer((props) => {
// props
const { attachmentId, handleAttachmentOperations, disabled } = props;
// store hooks
const { getUserDetails } = useMember();
const {
attachment: { getAttachmentById },
} = useIssueDetail();
// state
const [isDeleteIssueAttachmentModalOpen, setIsDeleteIssueAttachmentModalOpen] = useState(false);
// derived values
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
// hooks
const { isMobile } = usePlatformOS();
if (!attachment) return <></>;
return (
<>
{isDeleteIssueAttachmentModalOpen && (
<IssueAttachmentDeleteModal
isOpen={isDeleteIssueAttachmentModalOpen}
onClose={() => setIsDeleteIssueAttachmentModalOpen(false)}
handleAttachmentOperations={handleAttachmentOperations}
data={attachment}
/>
)}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
window.open(attachment.asset, "_blank");
}}
>
<div className="group flex items-center justify-between gap-3 h-11 hover:bg-custom-background-90 pl-9 pr-2">
<div className="flex items-center gap-3 text-sm truncate">
<div className="flex items-center gap-3 ">{getFileIcon(getFileExtension(attachment.asset), 18)}</div>
<Tooltip
tooltipContent={`${getFileName(attachment.attributes.name)}.${getFileExtension(attachment.asset)}`}
isMobile={isMobile}
>
<p className="text-custom-text-200 font-medium truncate">{`${getFileName(attachment.attributes.name)}.${getFileExtension(attachment.asset)}`}</p>
</Tooltip>
<span className="flex size-1.5 bg-custom-background-80 rounded-full" />
<span className="flex-shrink-0 text-custom-text-400">{convertBytesToSize(attachment.attributes.size)}</span>
</div>
<div className="flex items-center gap-3">
{attachment?.updated_by && (
<>
<Tooltip
isMobile={isMobile}
tooltipContent={`${
getUserDetails(attachment.updated_by)?.display_name ?? ""
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
>
<div className="flex items-center justify-center">
<ButtonAvatars showTooltip userIds={attachment?.updated_by} />
</div>
</Tooltip>
</>
)}
<CustomMenu ellipsis closeOnSelect placement="bottom-end" openOnHover disabled={disabled}>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsDeleteIssueAttachmentModalOpen(true);
}}
>
<div className="flex items-center gap-2">
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
<span>Delete</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</button>
</>
);
});
@@ -1,6 +1,4 @@
export * from "./attachment-detail";
export * from "./attachment-item-list";
export * from "./attachment-list-item";
export * from "./attachment-upload";
export * from "./attachments-list";
export * from "./delete-attachment-modal";
@@ -2,5 +2,3 @@ export * from "./root";
export * from "./links";
export * from "./link-detail";
export * from "./link-item";
export * from "./link-list";
@@ -1,116 +0,0 @@
"use client";
import { FC, useState } from "react";
import { Pencil, Trash2, LinkIcon, ExternalLink } from "lucide-react";
// ui
import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui";
// helpers
import { calculateTimeAgoShort } from "@/helpers/date-time.helper";
import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks
import { useIssueDetail } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal";
type TIssueLinkItem = {
linkId: string;
linkOperations: TLinkOperationsModal;
isNotAllowed: boolean;
};
export const IssueLinkItem: FC<TIssueLinkItem> = (props) => {
// props
const { linkId, linkOperations, isNotAllowed } = props;
// hooks
const {
toggleIssueLinkModal: toggleIssueLinkModalStore,
link: { getLinkById },
} = useIssueDetail();
// state
const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false);
const toggleIssueLinkModal = (modalToggle: boolean) => {
toggleIssueLinkModalStore(modalToggle);
setIsIssueLinkModalOpen(modalToggle);
};
const { isMobile } = usePlatformOS();
const linkDetail = getLinkById(linkId);
if (!linkDetail) return <></>;
return (
<>
<IssueLinkCreateUpdateModal
isModalOpen={isIssueLinkModalOpen}
handleModal={toggleIssueLinkModal}
linkOperations={linkOperations}
preloadedData={linkDetail}
/>
<div
key={linkId}
className="col-span-12 lg:col-span-6 xl:col-span-4 2xl:col-span-3 3xl:col-span-2 flex items-center justify-between gap-3 h-8 flex-shrink-0 px-3 bg-custom-background-90 border-[0.5px] border-custom-border-200 rounded"
>
<div className="flex items-center gap-2.5 truncate">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
<Tooltip tooltipContent={linkDetail.url} isMobile={isMobile}>
<span
className="truncate text-xs cursor-pointer"
onClick={() => {
copyTextToClipboard(linkDetail.url);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Link copied!",
message: "Link copied to clipboard",
});
}}
>
{linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
</span>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<p className="p-1 text-xs align-bottom leading-5 text-custom-text-300">
{calculateTimeAgoShort(linkDetail.created_at)}
</p>
<a
href={linkDetail.url}
target="_blank"
rel="noopener noreferrer"
className="relative grid place-items-center rounded p-1 text-custom-text-300 outline-none hover:text-custom-text-200 cursor-pointer hover:bg-custom-background-80"
>
<ExternalLink className="h-3.5 w-3.5 stroke-[1.5]" />
</a>
<CustomMenu
ellipsis
buttonClassName="text-custom-text-300 hover:text-custom-text-200"
placement="bottom-end"
closeOnSelect
disabled={isNotAllowed}
>
<CustomMenu.MenuItem
className="flex items-center gap-2"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleIssueLinkModal(true);
}}
>
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
Edit
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
className="flex items-center gap-2"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
linkOperations.remove(linkDetail.id);
}}
>
<Trash2 className="h-3 w-3" />
Delete
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</>
);
};
@@ -1,38 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
// computed
import { useIssueDetail } from "@/hooks/store";
import { IssueLinkItem } from "./link-item";
// hooks
import { TLinkOperations } from "./root";
type TLinkOperationsModal = Exclude<TLinkOperations, "create">;
type TLinkList = {
issueId: string;
linkOperations: TLinkOperationsModal;
disabled?: boolean;
};
export const LinkList: FC<TLinkList> = observer((props) => {
// props
const { issueId, linkOperations, disabled = false } = props;
// hooks
const {
link: { getLinksByIssueId },
} = useIssueDetail();
const issueLinks = getLinksByIssueId(issueId);
if (!issueLinks) return <></>;
return (
<div className="grid grid-cols-12 3xl:grid-cols-10 gap-2 px-9 py-4">
{issueLinks &&
issueLinks.length > 0 &&
issueLinks.map((linkId) => (
<IssueLinkItem key={linkId} linkId={linkId} linkOperations={linkOperations} isNotAllowed={disabled} />
))}
</div>
);
});
@@ -1,7 +1,7 @@
import { FC } from "react";
import { observer } from "mobx-react";
// computed
import { useIssueDetail } from "@/hooks/store";
import { useIssueDetail, useUser } from "@/hooks/store";
import { IssueLinkDetail } from "./link-detail";
// hooks
import { TLinkOperations } from "./root";
@@ -11,16 +11,18 @@ export type TLinkOperationsModal = Exclude<TLinkOperations, "create">;
export type TIssueLinkList = {
issueId: string;
linkOperations: TLinkOperationsModal;
disabled?: boolean;
};
export const IssueLinkList: FC<TIssueLinkList> = observer((props) => {
// props
const { issueId, linkOperations, disabled = false } = props;
const { issueId, linkOperations } = props;
// hooks
const {
link: { getLinksByIssueId },
} = useIssueDetail();
const {
membership: { currentProjectRole },
} = useUser();
const issueLinks = getLinksByIssueId(issueId);
@@ -31,7 +33,12 @@ export const IssueLinkList: FC<TIssueLinkList> = observer((props) => {
{issueLinks &&
issueLinks.length > 0 &&
issueLinks.map((linkId) => (
<IssueLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} isNotAllowed={disabled} />
<IssueLinkDetail
key={linkId}
linkId={linkId}
linkOperations={linkOperations}
isNotAllowed={currentProjectRole === 5 || currentProjectRole === 10}
/>
))}
</div>
);
@@ -126,7 +126,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
</div>
<div>
<IssueLinkList issueId={issueId} linkOperations={handleLinkOperations} disabled={disabled} />
<IssueLinkList issueId={issueId} linkOperations={handleLinkOperations} />
</div>
</div>
</>
@@ -224,8 +224,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
classNames="space-y-2 px-3 py-2"
root={scrollableContainerRef}
defaultHeight="100px"
horizontalOffset={100}
verticalOffset={200}
horizontalOffset={50}
>
<KanbanIssueDetailsBlock
cardRef={cardRef}
@@ -125,7 +125,6 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
defaultHeight="3rem"
root={containerRef}
classNames={`relative ${isLastChild && !isExpanded ? "" : "border-b border-b-custom-border-200"}`}
verticalOffset={100}
>
<IssueBlock
issueId={issueId}
@@ -102,7 +102,7 @@ export const ListGroup = observer((props: Props) => {
const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults;
const isPaginating = !!getIssueLoader(group.id);
useIntersectionObserver(containerRef, isPaginating ? null : intersectionElement, loadMoreIssues, `100% 0% 100% 0%`);
useIntersectionObserver(containerRef, isPaginating ? null : intersectionElement, loadMoreIssues, `50% 0% 50% 0%`);
const shouldLoadMore =
nextPageResults === undefined && groupIssueCount !== undefined && groupIssueIds
@@ -6,7 +6,6 @@ import useSWR from "swr";
import { IIssueDisplayFilterOptions } from "@plane/types";
// hooks
// components
import { EmptyState } from "@/components/common";
import { SpreadsheetView } from "@/components/issues/issue-layouts";
import { AllIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns";
import { SpreadsheetLayoutLoader } from "@/components/ui";
@@ -21,25 +20,17 @@ import {
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useGlobalView, useIssues, useUser } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
import { useIssuesActions } from "@/hooks/use-issues-actions";
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
// store
import emptyView from "@/public/empty-state/view.svg";
import { IssuePeekOverview } from "../../peek-overview";
import { IssueLayoutHOC } from "../issue-layout-HOC";
import { TRenderQuickActions } from "../list/list-view-types";
type Props = {
isDefaultView: boolean;
};
export const AllIssueLayoutRoot: React.FC<Props> = observer((props: Props) => {
const { isDefaultView } = props;
export const AllIssueLayoutRoot: React.FC = observer(() => {
// router
const { workspaceSlug, globalViewId } = useParams();
const router = useAppRouter();
const searchParams = useSearchParams();
const routeFilters: {
[key: string]: string;
@@ -59,9 +50,7 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props: Props) => {
const {
membership: { currentWorkspaceAllProjectsRole },
} = useUser();
const { fetchAllGlobalViews, getViewDetailsById } = useGlobalView();
const viewDetails = getViewDetailsById(globalViewId?.toString());
const { fetchAllGlobalViews } = useGlobalView();
// filter init from the query params
const routerFilterParams = () => {
@@ -97,7 +86,7 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props: Props) => {
if (workspaceSlug && globalViewId) fetchNextIssues(workspaceSlug.toString(), globalViewId.toString());
}, [fetchNextIssues, workspaceSlug, globalViewId]);
const { isLoading } = useSWR(
useSWR(
workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS_${workspaceSlug}` : null,
async () => {
if (workspaceSlug) {
@@ -173,21 +162,6 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props: Props) => {
[canEditProperties, removeIssue, updateIssue, archiveIssue]
);
// when the call is not loading and the view does not exist and the view is not a default view, show empty state
if (!isLoading && !viewDetails && !isDefaultView) {
return (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or you don't have permission to view it."
primaryButton={{
text: "Go to All Issues",
onClick: () => router.push(`/${workspaceSlug}/workspace-views/all-issues`),
}}
/>
);
}
if (getIssueLoader() === "init-loader" || !globalViewId || !groupedIssueIds) {
return <SpreadsheetLayoutLoader />;
}
@@ -4,7 +4,6 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// mobx store
// components
import { LogoSpinner } from "@/components/common";
import {
IssuePeekOverview,
ProjectViewAppliedFiltersRoot,
@@ -43,7 +42,7 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
// hooks
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW);
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}_${viewId}` : null,
async () => {
if (workspaceSlug && projectId && viewId) {
@@ -57,14 +56,6 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
if (!workspaceSlug || !projectId || !viewId) return <></>;
if (isLoading) {
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
}
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT_VIEW}>
<div className="relative flex h-full w-full flex-col overflow-hidden">
@@ -84,7 +84,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
"group selected-issue-row": isIssueSelected,
"border-[0.5px] border-custom-border-400": isIssueActive,
})}
verticalOffset={100}
>
<IssueRowDetails
issueId={issueId}
@@ -92,7 +92,7 @@ export const SpreadsheetTable = observer((props: Props) => {
const isPaginating = !!getIssueLoader();
useIntersectionObserver(containerRef, isPaginating ? null : intersectionElement, loadMoreIssues, `100% 0% 100% 0%`);
useIntersectionObserver(containerRef, isPaginating ? null : intersectionElement, loadMoreIssues, `50% 0% 50% 0%`);
const handleKeyBoardNavigation = useTableKeyboardNavigation();
@@ -190,10 +190,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: `${is_draft_issue ? "Draft issue" : "Issue"} created successfully.`,
actionItems: !is_draft_issue && response?.project_id && (
actionItems: !is_draft_issue && (
<CreateIssueToastActionItems
workspaceSlug={workspaceSlug.toString()}
projectId={response?.project_id}
projectId={projectId.toString()}
issueId={response.id}
/>
),
@@ -16,7 +16,7 @@ export const IssuePeekOverviewLoader: FC<TIssuePeekOverviewLoader> = (props) =>
const { isMobile } = usePlatformOS();
return (
<Loader className="w-full h-screen overflow-hidden p-5 space-y-6">
<Loader className="w-full h-full overflow-hidden p-5 space-y-6">
<div className="flex justify-between items-center gap-2">
<div className="flex items-center gap-2">
<Tooltip tooltipContent="Close the peek view" isMobile={isMobile}>
@@ -86,32 +86,31 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}
},
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
issues?.updateIssue &&
(await issues
.updateIssue(workspaceSlug, projectId, issueId, data)
.then(() => {
captureIssueEvent({
eventName: ISSUE_UPDATED,
payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
},
path: pathname,
});
})
.catch(() => {
captureIssueEvent({
eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue peek-overview" },
path: pathname,
});
setToast({
title: "Error!",
type: TOAST_TYPE.ERROR,
message: "Issue update failed",
});
}));
await issues
?.updateIssue(workspaceSlug, projectId, issueId, data)
.then(() => {
captureIssueEvent({
eventName: ISSUE_UPDATED,
payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
},
path: pathname,
});
})
.catch(() => {
captureIssueEvent({
eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue peek-overview" },
path: pathname,
});
setToast({
title: "Error!",
type: TOAST_TYPE.ERROR,
message: "Issue update failed",
});
});
},
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
@@ -82,10 +82,10 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
<div key={issueId}>
{issue && (
<div
className="group relative flex min-h-11 h-full w-full items-center gap-3 pr-2 py-1 transition-all hover:bg-custom-background-90"
className="group relative flex h-full w-full items-center gap-2 border-b border-custom-border-100 px-2 py-1 transition-all hover:bg-custom-background-90"
style={{ paddingLeft: `${spacingLeft}px` }}
>
<div className="flex size-5 items-center justify-center flex-shrink-0">
<div className="h-[22px] w-[22px] flex-shrink-0">
{/* disable the chevron when current issue is also the root issue*/}
{subIssueCount > 0 && !isCurrentIssueRoot && (
<>
@@ -95,7 +95,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
</div>
) : (
<div
className="flex h-full w-full cursor-pointer items-center justify-center text-custom-text-400 hover:text-custom-text-300"
className="flex h-full w-full cursor-pointer items-center justify-center rounded-sm transition-all hover:bg-custom-background-80"
onClick={async () => {
if (!subIssueHelpers.issue_visibility.includes(issueId)) {
setSubIssueHelpers(parentIssueId, "preview_loader", issueId);
@@ -106,10 +106,10 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
}}
>
<ChevronRight
className={cn("size-3.5 transition-all", {
className={cn("h-3 w-3 transition-all", {
"rotate-90": subIssueHelpers.issue_visibility.includes(issue.id),
})}
strokeWidth={2.5}
strokeWidth={2}
/>
</div>
)}
@@ -119,9 +119,9 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
<div className="flex w-full cursor-pointer items-center gap-2">
<div
className="h-2 w-2 flex-shrink-0 rounded-full"
className="h-[6px] w-[6px] flex-shrink-0 rounded-full"
style={{
backgroundColor: currentIssueStateDetail?.color ?? "#737373",
backgroundColor: currentIssueStateDetail?.color,
}}
/>
<div className="flex-shrink-0 text-xs text-custom-text-200">
@@ -166,33 +166,6 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={() =>
subIssueOperations.copyText(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`)
}
>
<div className="flex items-center gap-2">
<LinkIcon className="h-3.5 w-3.5" strokeWidth={2} />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
{disabled && (
<CustomMenu.MenuItem
onClick={() => {
issue.project_id &&
subIssueOperations.removeSubIssue(workspaceSlug, issue.project_id, parentIssueId, issue.id);
}}
>
<div className="flex items-center gap-2">
<X className="h-3.5 w-3.5" strokeWidth={2} />
<span>Remove parent issue</span>
</div>
</CustomMenu.MenuItem>
)}
<hr className="border-custom-border-300" />
{disabled && (
<CustomMenu.MenuItem
onClick={() => {
@@ -206,27 +179,55 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={() =>
subIssueOperations.copyText(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`)
}
>
<div className="flex items-center gap-2">
<LinkIcon className="h-3.5 w-3.5" strokeWidth={2} />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
{disabled && (
<>
{subIssueHelpers.issue_loader.includes(issue.id) ? (
<div className="flex h-[22px] w-[22px] flex-shrink-0 cursor-not-allowed items-center justify-center overflow-hidden rounded-sm transition-all">
<Loader width={14} strokeWidth={2} className="animate-spin" />
</div>
) : (
<div
className="invisible flex h-[22px] w-[22px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80 group-hover:visible"
onClick={() => {
issue.project_id &&
subIssueOperations.removeSubIssue(workspaceSlug, issue.project_id, parentIssueId, issue.id);
}}
>
<X width={14} strokeWidth={2} />
</div>
)}
</>
)}
</div>
)}
{/* should not expand the current issue if it is also the root issue*/}
{subIssueHelpers.issue_visibility.includes(issueId) &&
issue.project_id &&
subIssueCount > 0 &&
!isCurrentIssueRoot && (
<IssueList
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
parentIssueId={issue.id}
rootIssueId={rootIssueId}
spacingLeft={spacingLeft + 22}
disabled={disabled}
handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations}
/>
)}
{subIssueHelpers.issue_visibility.includes(issueId) && issue.project_id && subIssueCount > 0 && !isCurrentIssueRoot && (
<IssueList
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
parentIssueId={issue.id}
rootIssueId={rootIssueId}
spacingLeft={spacingLeft + 22}
disabled={disabled}
handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations}
/>
)}
</div>
);
});
@@ -42,24 +42,31 @@ export const IssueList: FC<IIssueList> = observer((props) => {
const subIssueIds = subIssuesByIssueId(parentIssueId);
return (
<div className="relative">
{subIssueIds &&
subIssueIds.length > 0 &&
subIssueIds.map((issueId) => (
<Fragment key={issueId}>
<IssueListItem
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={parentIssueId}
rootIssueId={rootIssueId}
issueId={issueId}
spacingLeft={spacingLeft}
disabled={disabled}
handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations}
/>
</Fragment>
))}
</div>
<>
<div className="relative">
{subIssueIds &&
subIssueIds.length > 0 &&
subIssueIds.map((issueId) => (
<Fragment key={issueId}>
<IssueListItem
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={parentIssueId}
rootIssueId={rootIssueId}
issueId={issueId}
spacingLeft={spacingLeft}
disabled={disabled}
handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations}
/>
</Fragment>
))}
<div
className={`absolute bottom-0 top-0 ${spacingLeft > 10 ? `border-l border-custom-border-100` : ``}`}
style={{ left: `${spacingLeft - 12}px` }}
/>
</div>
</>
);
});
+12 -10
View File
@@ -395,16 +395,18 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
</div>
{subIssueHelpers.issue_visibility.includes(parentIssueId) && (
<IssueList
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={parentIssueId}
rootIssueId={parentIssueId}
spacingLeft={10}
disabled={!disabled}
handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations}
/>
<div className="border border-b-0 border-custom-border-100">
<IssueList
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={parentIssueId}
rootIssueId={parentIssueId}
spacingLeft={10}
disabled={!disabled}
handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations}
/>
</div>
)}
</>
) : (
@@ -6,14 +6,9 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// components
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "@/components/issues";
// constants
import {
EIssuesStoreType,
EIssueFilterType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
EIssueLayoutTypes,
} from "@/constants/issue";
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, EIssueLayoutTypes } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel } from "@/hooks/store";
@@ -100,6 +95,8 @@ export const ProfileIssuesFilter = observer(() => {
[workspaceSlug, updateFilters, userId]
);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
return (
<div className="relative flex items-center justify-end gap-2">
<LayoutSelection
@@ -108,7 +105,7 @@ export const ProfileIssuesFilter = observer(() => {
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
<FilterSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
@@ -10,7 +10,7 @@ import { IProject, TProjectPublishLayouts, TProjectPublishSettings } from "@plan
// ui
import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast, CustomSelect, ModalCore, EModalWidth } from "@plane/ui";
// helpers
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
import { SPACE_BASE_URL } from "@/helpers/common.helper";
import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks
import { useProjectPublish } from "@/hooks/store";
@@ -156,8 +156,7 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
});
}, [projectPublishSettings, reset]);
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
const publishLink = `${SPACE_APP_URL}/issues/${projectPublishSettings?.anchor}`;
const publishLink = `${SPACE_BASE_URL}/issues/${projectPublishSettings?.anchor}`;
const handleCopyLink = () =>
copyTextToClipboard(publishLink).then(() =>
@@ -16,21 +16,21 @@ type TNotificationAppSidebarOption = {
export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = observer((props) => {
const { workspaceSlug, isSidebarCollapsed } = props;
// hooks
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
const { totalUnreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
useSWR(
workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug) : null
);
if (unreadNotificationsCount.total_unread_notifications_count <= 0) return <></>;
if (totalUnreadNotificationsCount <= 0) return <></>;
if (isSidebarCollapsed)
return <div className="absolute right-3.5 top-2 h-2 w-2 rounded-full bg-custom-primary-300" />;
return (
<div className="ml-auto px-2.5 py-0.5 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{getNumberCount(unreadNotificationsCount.total_unread_notifications_count)}
<div className="text-[8px] ml-auto bg-custom-primary-100 text-white p-1 py-0.5 rounded-full">
{getNumberCount(totalUnreadNotificationsCount)}
</div>
);
});
@@ -3,15 +3,11 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Bell } from "lucide-react";
import { Breadcrumbs, Tooltip } from "@plane/ui";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { SidebarHamburgerToggle } from "@/components/core";
import { NotificationSidebarHeaderOptions } from "@/components/workspace-notifications";
// helpers
import { getNumberCount } from "@/helpers/string.helper";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
type TNotificationSidebarHeader = {
workspaceSlug: string;
@@ -20,8 +16,6 @@ type TNotificationSidebarHeader = {
export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observer((props) => {
const { workspaceSlug, notificationsCount } = props;
// hooks
const { isMobile } = usePlatformOS();
if (!workspaceSlug) return <></>;
return (
@@ -38,15 +32,9 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
label={
<div className="flex items-center gap-2">
<div className="font-medium">Notifications</div>
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${notificationsCount} ${notificationsCount > 1 ? "notifications" : "notification"} in this workspace`}
position="bottom"
>
<div className="px-2.5 py-0.5 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{getNumberCount(notificationsCount)}
</div>
</Tooltip>
<div className="rounded-full text-xs px-1.5 py-0.5 bg-custom-primary-100/20">
{notificationsCount}
</div>
</div>
}
icon={<Bell className="h-4 w-4 text-custom-text-300" />}
@@ -1,6 +1,6 @@
export const NotificationsLoader = () => (
<div className="divide-y divide-custom-border-100 animate-pulse overflow-hidden">
{[...Array(8)].map((i) => (
{[...Array(3)].map((i) => (
<div key={i} className="flex w-full items-center gap-4 p-3">
<span className="min-h-12 min-w-12 bg-custom-background-80 rounded-full" />
<div className="flex flex-col gap-2.5 w-full">
@@ -3,7 +3,6 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { Clock } from "lucide-react";
import { TCurrentSelectedNotification } from "@plane/types";
import { Avatar } from "@plane/ui";
// components
import { NotificationOption } from "@/components/workspace-notifications";
@@ -13,7 +12,7 @@ import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/he
import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
// hooks
import { useIssueDetail, useNotification, useWorkspaceNotifications } from "@/hooks/store";
import { useIssueDetail, useNotification } from "@/hooks/store";
type TNotificationItem = {
workspaceSlug: string;
@@ -23,7 +22,6 @@ type TNotificationItem = {
export const NotificationItem: FC<TNotificationItem> = observer((props) => {
const { workspaceSlug, notificationId } = props;
// hooks
const { currentSelectedNotification, setCurrentSelectedNotification } = useWorkspaceNotifications();
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
// states
@@ -46,28 +44,14 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
!isSnoozeStateModalOpen &&
!customSnoozeModal
) {
const currentSelectedNotificationPayload: TCurrentSelectedNotification = {
workspace_slug: workspaceSlug,
project_id: projectId,
issue_id: issueId,
notification_id: notification?.id,
is_inbox_issue: notification?.is_inbox_issue || false,
};
setCurrentSelectedNotification(currentSelectedNotificationPayload);
setPeekIssue({ workspaceSlug, projectId, issueId });
// make the notification as read
if (notification.read_at === null) {
if (notification.read_at === null)
try {
await markNotificationAsRead(workspaceSlug);
} catch (error) {
console.error(error);
}
}
if (notification?.is_inbox_issue === false) {
setPeekIssue({ workspaceSlug, projectId, issueId });
} else {
}
}
};
@@ -77,10 +61,8 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
<div
className={cn(
"relative p-3 py-4 flex items-center gap-2 border-b border-custom-border-200 cursor-pointer transition-all group",
currentSelectedNotification && currentSelectedNotification?.notification_id === notification?.id
? "bg-custom-background-80/30"
: "",
notification.read_at === null ? "bg-custom-primary-100/5" : ""
// peekIssue && peekIssue?.issueId === issueId ? "bg-custom-background-80" : "
)}
onClick={handleNotificationIssuePeekOverview}
>

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