Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5fa46929b |
@@ -170,7 +170,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Admin Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
uses: ./.github/actions/buildpush-action
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Web Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
uses: ./.github/actions/buildpush-action
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Space Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
uses: ./.github/actions/buildpush-action
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -248,7 +248,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Live Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
uses: ./.github/actions/buildpush-action
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -274,7 +274,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Backend Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
uses: ./.github/actions/buildpush-action
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Proxy Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
uses: ./.github/actions/buildpush-action
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -320,7 +320,7 @@ jobs:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_type == 'Build' }}
|
||||
name: Attach Assets to Build
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
needs: [ branch_build_setup ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -340,7 +340,7 @@ jobs:
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
|
||||
|
||||
publish_release:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
|
||||
name: Build Release
|
||||
@@ -353,7 +353,7 @@ jobs:
|
||||
branch_build_push_space,
|
||||
branch_build_push_live,
|
||||
branch_build_push_apiserver,
|
||||
branch_build_push_proxy,
|
||||
branch_build_push_proxy
|
||||
]
|
||||
env:
|
||||
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
@@ -380,4 +380,4 @@ jobs:
|
||||
${{ github.workspace }}/deploy/selfhost/setup.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
+1
-1
@@ -41,7 +41,7 @@
|
||||
"@plane/eslint-config": "*",
|
||||
"@plane/typescript-config": "*",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/zxcvbn": "^4.4.4",
|
||||
|
||||
@@ -57,6 +57,5 @@ ADMIN_BASE_URL=
|
||||
SPACE_BASE_URL=
|
||||
APP_BASE_URL=
|
||||
|
||||
|
||||
# Hard delete files after days
|
||||
HARD_DELETE_AFTER_DAYS=60
|
||||
HARD_DELETE_AFTER_DAYS=60
|
||||
|
||||
@@ -212,7 +212,7 @@ class IssueSerializer(BaseSerializer):
|
||||
updated_by_id = instance.updated_by_id
|
||||
|
||||
if assignees is not None:
|
||||
IssueAssignee.objects.filter(issue=instance).delete()
|
||||
IssueAssignee.objects.filter(issue=instance).delete(soft=False)
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
IssueAssignee(
|
||||
@@ -229,7 +229,7 @@ class IssueSerializer(BaseSerializer):
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
IssueLabel.objects.filter(issue=instance).delete()
|
||||
IssueLabel.objects.filter(issue=instance).delete(soft=False)
|
||||
IssueLabel.objects.bulk_create(
|
||||
[
|
||||
IssueLabel(
|
||||
|
||||
@@ -404,7 +404,7 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
cycle.delete(soft=False)
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
entity_type="cycle",
|
||||
|
||||
@@ -227,10 +227,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -238,11 +235,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@@ -16,7 +16,6 @@ from django.db.models import (
|
||||
Q,
|
||||
Value,
|
||||
When,
|
||||
Subquery,
|
||||
)
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -49,7 +48,6 @@ from plane.db.models import (
|
||||
Label,
|
||||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
)
|
||||
|
||||
from .base import BaseAPIView
|
||||
@@ -205,10 +203,12 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
Q(issue_cycle__cycle__deleted_at__isnull=True),
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
|
||||
@@ -169,7 +169,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
updated_by_id = instance.updated_by_id
|
||||
|
||||
if assignees is not None:
|
||||
DraftIssueAssignee.objects.filter(draft_issue=instance).delete()
|
||||
DraftIssueAssignee.objects.filter(draft_issue=instance).delete(
|
||||
soft=False
|
||||
)
|
||||
DraftIssueAssignee.objects.bulk_create(
|
||||
[
|
||||
DraftIssueAssignee(
|
||||
@@ -186,7 +188,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
DraftIssueLabel.objects.filter(draft_issue=instance).delete()
|
||||
DraftIssueLabel.objects.filter(draft_issue=instance).delete(
|
||||
soft=False
|
||||
)
|
||||
DraftIssueLabel.objects.bulk_create(
|
||||
[
|
||||
DraftIssueLabel(
|
||||
@@ -204,7 +208,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
|
||||
if cycle_id != "not_provided":
|
||||
DraftIssueCycle.objects.filter(draft_issue=instance).delete()
|
||||
if cycle_id:
|
||||
if cycle_id is not None:
|
||||
DraftIssueCycle.objects.create(
|
||||
cycle_id=cycle_id,
|
||||
draft_issue=instance,
|
||||
|
||||
@@ -201,7 +201,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
updated_by_id = instance.updated_by_id
|
||||
|
||||
if assignees is not None:
|
||||
IssueAssignee.objects.filter(issue=instance).delete()
|
||||
IssueAssignee.objects.filter(issue=instance).delete(soft=False)
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
IssueAssignee(
|
||||
@@ -218,7 +218,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
IssueLabel.objects.filter(issue=instance).delete()
|
||||
IssueLabel.objects.filter(issue=instance).delete(soft=False)
|
||||
IssueLabel.objects.bulk_create(
|
||||
[
|
||||
IssueLabel(
|
||||
|
||||
@@ -110,10 +110,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
if x_axis in ["labels__id"] or segment in ["labels__id"]:
|
||||
label_details = (
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
**filters,
|
||||
labels__id__isnull=False
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
workspace__slug=slug, **filters, labels__id__isnull=False
|
||||
)
|
||||
.distinct("labels__id")
|
||||
.order_by("labels__id")
|
||||
|
||||
@@ -144,11 +144,6 @@ class CycleViewSet(BaseViewSet):
|
||||
distinct=True,
|
||||
filter=~Q(
|
||||
issue_cycle__issue__assignees__id__isnull=True
|
||||
)
|
||||
& (
|
||||
Q(
|
||||
issue_cycle__issue__issue_assignee__deleted_at__isnull=True
|
||||
)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -496,7 +491,7 @@ class CycleViewSet(BaseViewSet):
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# TODO: Soft delete the cycle break the onetoone relationship with cycle issue
|
||||
cycle.delete()
|
||||
cycle.delete(soft=False)
|
||||
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.core import serializers
|
||||
from django.db.models import F, Func, OuterRef, Q, Subquery
|
||||
from django.db.models import F, Func, OuterRef, Q, Case, When
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
@@ -103,10 +103,12 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
)
|
||||
.filter(**filters)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
|
||||
@@ -42,7 +42,6 @@ from plane.db.models import (
|
||||
Project,
|
||||
Widget,
|
||||
WorkspaceMember,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
@@ -192,13 +191,7 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
).select_related("issue"),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -225,9 +218,9 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -236,11 +229,8 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -248,11 +238,9 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__module__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -372,13 +360,7 @@ def dashboard_created_issues(self, request, slug):
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -405,9 +387,9 @@ def dashboard_created_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -416,11 +398,8 @@ def dashboard_created_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -428,11 +407,9 @@ def dashboard_created_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__module__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
|
||||
# Django import
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch, Subquery
|
||||
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch, Case, When
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
@@ -26,7 +26,6 @@ from plane.db.models import (
|
||||
FileAsset,
|
||||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
@@ -114,10 +113,12 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -148,9 +149,9 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -159,11 +160,8 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -171,11 +169,9 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__module__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -202,10 +198,8 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__labels__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
@@ -317,11 +311,9 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(
|
||||
issue__label_issue__deleted_at__isnull=True
|
||||
)
|
||||
& Q(issue__labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -331,9 +323,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(issue__assignees__id__isnull=True)
|
||||
& Q(
|
||||
issue__assignees__member_project__is_active=True
|
||||
),
|
||||
& Q(issue__assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -386,10 +376,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -397,10 +384,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -523,12 +507,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(
|
||||
issue__label_issue__deleted_at__isnull=True
|
||||
)
|
||||
),
|
||||
filter=~Q(issue__labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -536,12 +515,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__assignees__id__isnull=True)
|
||||
& Q(
|
||||
issue__issue_assignee__deleted_at__isnull=True
|
||||
)
|
||||
),
|
||||
filter=~Q(issue__assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -586,10 +560,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue__labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -597,10 +568,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__assignees__id__isnull=True)
|
||||
& Q(issue__issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue__assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery
|
||||
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Case, When
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
@@ -27,7 +27,6 @@ from plane.db.models import (
|
||||
IssueLink,
|
||||
IssueSubscriber,
|
||||
IssueReaction,
|
||||
CycleIssue
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -66,10 +65,12 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
|
||||
@@ -14,7 +14,8 @@ from django.db.models import (
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
Subquery,
|
||||
When,
|
||||
Case,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
@@ -43,7 +44,6 @@ from plane.db.models import (
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -86,10 +86,12 @@ class IssueListEndpoint(BaseAPIView):
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -216,10 +218,12 @@ class IssueViewSet(BaseViewSet):
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -485,9 +489,9 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -496,11 +500,8 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -508,11 +509,9 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__module__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -591,9 +590,9 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -602,11 +601,8 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -614,11 +610,9 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__module__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -782,10 +776,12 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
)
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -894,9 +890,9 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -905,11 +901,8 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -917,11 +910,9 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__module__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@@ -11,7 +11,8 @@ from django.db.models import (
|
||||
UUIDField,
|
||||
Value,
|
||||
CharField,
|
||||
Subquery,
|
||||
Case,
|
||||
When,
|
||||
)
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models.functions import Coalesce
|
||||
@@ -35,7 +36,6 @@ from plane.db.models import (
|
||||
Issue,
|
||||
FileAsset,
|
||||
IssueLink,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
@@ -94,10 +94,12 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -128,9 +130,9 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& (Q(label_issue__deleted_at__isnull=True))
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -139,11 +141,8 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -290,7 +289,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
IssueRelationSerializer(issue_relation).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
)
|
||||
issue_relation.delete()
|
||||
issue_relation.delete(soft=False)
|
||||
issue_activity.delay(
|
||||
type="issue_relation.activity.deleted",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
|
||||
@@ -3,7 +3,16 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import OuterRef, Func, F, Q, Value, UUIDField, Subquery
|
||||
from django.db.models import (
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Q,
|
||||
Value,
|
||||
UUIDField,
|
||||
Case,
|
||||
When,
|
||||
)
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
@@ -22,7 +31,6 @@ from plane.db.models import (
|
||||
Issue,
|
||||
IssueLink,
|
||||
FileAsset,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
@@ -43,10 +51,12 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -77,9 +87,9 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -88,11 +98,8 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -100,11 +107,9 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__module__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
from django.db.models import F, Func, OuterRef, Q, Subquery
|
||||
from django.db.models import (
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
Case,
|
||||
When,
|
||||
)
|
||||
|
||||
# Django Imports
|
||||
from django.utils import timezone
|
||||
@@ -23,7 +30,6 @@ from plane.db.models import (
|
||||
IssueLink,
|
||||
ModuleIssue,
|
||||
Project,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -62,10 +68,12 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
|
||||
@@ -42,56 +42,47 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
issues = search_issues(query, issues)
|
||||
|
||||
if parent == "true" and issue_id:
|
||||
issue = Issue.issue_objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~Q(pk=issue.parent_id),
|
||||
~Q(parent_id=issue_id),
|
||||
)
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id)
|
||||
)
|
||||
if issue_relation == "true" and issue_id:
|
||||
issue = Issue.issue_objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~(
|
||||
Q(issue_related__issue=issue)
|
||||
& Q(issue_related__deleted_at__isnull=True)
|
||||
),
|
||||
~(
|
||||
Q(issue_relation__related_issue=issue)
|
||||
& Q(issue_relation__deleted_at__isnull=True)
|
||||
),
|
||||
)
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~Q(
|
||||
issue_related__issue=issue,
|
||||
issue_related__deleted_at__isnull=True,
|
||||
),
|
||||
~Q(
|
||||
issue_relation__related_issue=issue,
|
||||
issue_related__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
if sub_issue == "true" and issue_id:
|
||||
issue = Issue.issue_objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
|
||||
if issue.parent:
|
||||
issues = issues.filter(~Q(pk=issue.parent_id))
|
||||
|
||||
if cycle == "true":
|
||||
issues = issues.exclude(
|
||||
Q(issue_cycle__isnull=False)
|
||||
& Q(issue_cycle__deleted_at__isnull=True)
|
||||
)
|
||||
issues = issues.exclude(issue_cycle__isnull=False)
|
||||
|
||||
if module:
|
||||
issues = issues.exclude(
|
||||
Q(issue_module__module=module)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
)
|
||||
issues = issues.exclude(issue_module__module=module)
|
||||
|
||||
if target_date == "none":
|
||||
issues = issues.filter(target_date__isnull=True)
|
||||
|
||||
|
||||
if ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member=self.request.user,
|
||||
is_active=True,
|
||||
role=5,
|
||||
role=5
|
||||
).exists():
|
||||
issues = issues.filter(created_by=self.request.user)
|
||||
issues = issues.filter(
|
||||
created_by=self.request.user
|
||||
)
|
||||
|
||||
return Response(
|
||||
issues.values(
|
||||
|
||||
@@ -9,7 +9,8 @@ from django.db.models import (
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
Subquery,
|
||||
Case,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -37,7 +38,6 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
Project,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -208,10 +208,12 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -242,9 +244,9 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -253,11 +255,8 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -265,11 +264,9 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__module__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -289,10 +286,12 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -553,9 +552,7 @@ class IssueViewViewSet(BaseViewSet):
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView)
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
project_view = IssueView.objects.get(
|
||||
pk=pk,
|
||||
|
||||
@@ -8,11 +8,12 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import (
|
||||
F,
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
Subquery,
|
||||
OuterRef,
|
||||
Case,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -35,6 +36,7 @@ from plane.db.models import (
|
||||
DraftIssue,
|
||||
CycleIssue,
|
||||
ModuleIssue,
|
||||
DraftIssueModule,
|
||||
DraftIssueCycle,
|
||||
Workspace,
|
||||
FileAsset,
|
||||
@@ -54,11 +56,14 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "draft_issue_module__module"
|
||||
)
|
||||
.annotate(cycle_id=F("draft_issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
DraftIssueCycle.objects.filter(
|
||||
draft_issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
draft_issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("draft_issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -66,9 +71,9 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& (Q(draft_label_issue__deleted_at__isnull=True))
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -77,11 +82,8 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(draft_issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -89,12 +91,12 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"draft_issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(draft_issue_module__module_id__isnull=True)
|
||||
& Q(
|
||||
draft_issue_module__module__archived_at__isnull=True
|
||||
)
|
||||
& Q(draft_issue_module__deleted_at__isnull=True)
|
||||
filter=~Q(draft_issue_module__module_id__isnull=True)
|
||||
& Q(
|
||||
draft_issue_module__module__archived_at__isnull=True
|
||||
)
|
||||
& Q(
|
||||
draft_issue_module__module__deleted_at__isnull=True
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
|
||||
@@ -14,7 +14,6 @@ from django.db.models import (
|
||||
Q,
|
||||
Value,
|
||||
When,
|
||||
Subquery,
|
||||
)
|
||||
from django.db.models.fields import DateField
|
||||
from django.db.models.functions import Cast, ExtractWeek
|
||||
@@ -122,10 +121,12 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
|
||||
@@ -130,9 +130,7 @@ def get_label_details(slug, filters):
|
||||
"""Fetch label details if required"""
|
||||
return (
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
**filters,
|
||||
labels__id__isnull=False & Q(label_issue__deleted_at__isnull=True),
|
||||
workspace__slug=slug, **filters, labels__id__isnull=False
|
||||
)
|
||||
.distinct("labels__id")
|
||||
.order_by("labels__id")
|
||||
|
||||
@@ -32,14 +32,14 @@ app.conf.beat_schedule = {
|
||||
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
|
||||
"schedule": crontab(minute="*/5"),
|
||||
},
|
||||
"check-every-day-to-delete-hard-delete": {
|
||||
"task": "plane.bgtasks.deletion_task.hard_delete",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"check-every-day-to-delete-api-logs": {
|
||||
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"check-every-day-to-delete-hard-delete": {
|
||||
"task": "plane.bgtasks.deletion_task.hard_delete",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"run-every-6-hours-for-instance-trace": {
|
||||
"task": "plane.license.bgtasks.tracer.instance_traces",
|
||||
"schedule": crontab(hour="*/6"),
|
||||
|
||||
@@ -68,6 +68,7 @@ class Command(BaseCommand):
|
||||
Key="test_permission_check.txt",
|
||||
Body=b"Test",
|
||||
)
|
||||
self.stdout.write("PutObject permission granted.")
|
||||
permissions["s3:PutObject"] = True
|
||||
# Clean up
|
||||
except ClientError as e:
|
||||
@@ -149,47 +150,23 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Create a session using the credentials from Django settings
|
||||
|
||||
# Check if the bucket exists
|
||||
s3_client = self.get_s3_client()
|
||||
# Get the bucket name from the environment
|
||||
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
|
||||
if not bucket_name:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
"Please set the AWS_S3_BUCKET_NAME environment variable."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.stdout.write(self.style.NOTICE("Checking bucket..."))
|
||||
# Check if the bucket exists
|
||||
try:
|
||||
# Check if the bucket exists
|
||||
s3_client = self.get_s3_client()
|
||||
# Get the bucket name from the environment
|
||||
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
self.stdout.write(self.style.NOTICE("Checking bucket..."))
|
||||
# Check if the bucket exists
|
||||
s3_client.head_bucket(Bucket=bucket_name)
|
||||
except ClientError as e:
|
||||
error_code = e.response["Error"]["Code"]
|
||||
if error_code == "404":
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f"Bucket '{bucket_name}' does not exist.")
|
||||
)
|
||||
return
|
||||
else:
|
||||
self.stdout.write(f"Error: {e}")
|
||||
# If the bucket exists, print a success message
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Bucket '{bucket_name}' exists.")
|
||||
)
|
||||
|
||||
try:
|
||||
# If the bucket exists, print a success message
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Bucket '{bucket_name}' exists.")
|
||||
)
|
||||
|
||||
# Check the permissions of the access key
|
||||
permissions = self.check_s3_permissions(bucket_name)
|
||||
except ClientError as e:
|
||||
self.stdout.write(f"Error: {e}")
|
||||
except Exception as e:
|
||||
self.stdout.write(f"Error: {e}")
|
||||
# If the access key has the required permissions
|
||||
try:
|
||||
|
||||
if all(permissions.values()):
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
@@ -198,26 +175,35 @@ class Command(BaseCommand):
|
||||
)
|
||||
# Making the existing objects public
|
||||
self.make_objects_public(bucket_name)
|
||||
return
|
||||
except Exception as e:
|
||||
self.stdout.write(f"Error: {e}")
|
||||
|
||||
# write the bucket policy to a file
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"Generating permissions.json for manual bucket policy update."
|
||||
)
|
||||
)
|
||||
try:
|
||||
# Writing to a file
|
||||
with open("permissions.json", "w") as f:
|
||||
f.write(json.dumps(self.generate_bucket_policy(bucket_name)))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"Permissions have been written to permissions.json."
|
||||
# If the access key does not have PutBucketPolicy permission
|
||||
# write the bucket policy to a file
|
||||
if (
|
||||
all(
|
||||
{
|
||||
k: v
|
||||
for k, v in permissions.items()
|
||||
if k != "s3:PutBucketPolicy"
|
||||
}.values()
|
||||
)
|
||||
)
|
||||
return
|
||||
except IOError as e:
|
||||
self.stdout.write(f"Error writing permissions.json: {e}")
|
||||
return
|
||||
and not permissions["s3:PutBucketPolicy"]
|
||||
):
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"Access key does not have PutBucketPolicy permission."
|
||||
)
|
||||
)
|
||||
# Writing to a file
|
||||
with open("permissions.json", "w") as f:
|
||||
f.write(
|
||||
json.dumps(self.generate_bucket_policy(bucket_name))
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"Permissions have been written to permissions.json."
|
||||
)
|
||||
)
|
||||
return
|
||||
except Exception as ex:
|
||||
# Handle any other exception
|
||||
self.stdout.write(self.style.ERROR(f"An error occurred: {ex}"))
|
||||
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
# Generated by Django 4.2.15 on 2024-10-22 08:00
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.db.models.manager
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0081_remove_globalview_created_by_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name="issue",
|
||||
managers=[
|
||||
("issue_objects", django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="cycleissue",
|
||||
name="issue",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="issue_cycle",
|
||||
to="db.issue",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="draftissuecycle",
|
||||
name="draft_issue",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="draft_issue_cycle",
|
||||
to="db.draftissue",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="cycleissue",
|
||||
unique_together={("issue", "cycle", "deleted_at")},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="draftissuecycle",
|
||||
unique_together={("draft_issue", "cycle", "deleted_at")},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="cycleissue",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("deleted_at__isnull", True)),
|
||||
fields=("cycle", "issue"),
|
||||
name="cycle_issue_when_deleted_at_null",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="draftissuecycle",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("deleted_at__isnull", True)),
|
||||
fields=("draft_issue", "cycle"),
|
||||
name="draft_issue_cycle_when_deleted_at_null",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -110,7 +110,7 @@ class CycleIssue(ProjectBaseModel):
|
||||
Cycle Issues
|
||||
"""
|
||||
|
||||
issue = models.ForeignKey(
|
||||
issue = models.OneToOneField(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="issue_cycle"
|
||||
)
|
||||
cycle = models.ForeignKey(
|
||||
@@ -118,14 +118,6 @@ class CycleIssue(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "cycle", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["cycle", "issue"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="cycle_issue_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Cycle Issue"
|
||||
verbose_name_plural = "Cycle Issues"
|
||||
db_table = "cycle_issues"
|
||||
|
||||
@@ -234,7 +234,7 @@ class DraftIssueCycle(WorkspaceBaseModel):
|
||||
Draft Issue Cycles
|
||||
"""
|
||||
|
||||
draft_issue = models.ForeignKey(
|
||||
draft_issue = models.OneToOneField(
|
||||
"db.DraftIssue",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="draft_issue_cycle",
|
||||
@@ -244,14 +244,6 @@ class DraftIssueCycle(WorkspaceBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["draft_issue", "cycle", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["draft_issue", "cycle"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="draft_issue_cycle_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Draft Issue Cycle"
|
||||
verbose_name_plural = "Draft Issue Cycles"
|
||||
db_table = "draft_issue_cycles"
|
||||
|
||||
@@ -91,7 +91,6 @@ class IssueManager(SoftDeletionManager):
|
||||
| models.Q(issue_inbox__status=2)
|
||||
| models.Q(issue_inbox__isnull=True)
|
||||
)
|
||||
.filter(deleted_at__isnull=True)
|
||||
.filter(state__is_triage=False)
|
||||
.exclude(archived_at__isnull=False)
|
||||
.exclude(project__archived_at__isnull=False)
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.db import models
|
||||
# Module imports
|
||||
from plane.utils.html_processor import strip_tags
|
||||
|
||||
from .project import ProjectBaseModel
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
# Module import
|
||||
from .base import BaseModel
|
||||
from .project import ProjectBaseModel
|
||||
from .workspace import WorkspaceBaseModel
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
@@ -421,7 +421,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
updated_by_id = instance.updated_by_id
|
||||
|
||||
if assignees is not None:
|
||||
IssueAssignee.objects.filter(issue=instance).delete()
|
||||
IssueAssignee.objects.filter(issue=instance).delete(soft=False)
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
IssueAssignee(
|
||||
@@ -438,7 +438,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
IssueLabel.objects.filter(issue=instance).delete()
|
||||
IssueLabel.objects.filter(issue=instance).delete(soft=False)
|
||||
IssueLabel.objects.bulk_create(
|
||||
[
|
||||
IssueLabel(
|
||||
|
||||
@@ -35,16 +35,8 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by):
|
||||
}
|
||||
|
||||
annotations_map = {
|
||||
"assignee_ids": (
|
||||
"assignees__id",
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True),
|
||||
),
|
||||
"label_ids": (
|
||||
"labels__id",
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
"assignee_ids": ("assignees__id", ~Q(assignees__id__isnull=True)),
|
||||
"label_ids": ("labels__id", ~Q(labels__id__isnull=True)),
|
||||
"module_ids": (
|
||||
"issue_module__module_id",
|
||||
~Q(issue_module__module_id__isnull=True),
|
||||
|
||||
@@ -20,7 +20,6 @@ from django.db.models import (
|
||||
OuterRef,
|
||||
Func,
|
||||
CharField,
|
||||
Subquery,
|
||||
)
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
@@ -63,7 +62,6 @@ from plane.db.models import (
|
||||
IssueVote,
|
||||
ProjectPublicMember,
|
||||
FileAsset,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
@@ -109,10 +107,12 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -704,10 +704,12 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -715,9 +717,9 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -726,11 +728,8 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -740,7 +739,7 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True),
|
||||
& Q(issue_module__module__deleted_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@@ -25,22 +25,17 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by):
|
||||
}
|
||||
|
||||
annotations_map = {
|
||||
"assignee_ids": (
|
||||
"assignees__id",
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True),
|
||||
),
|
||||
"assignee_ids": ("assignees__id", ~Q(assignees__id__isnull=True)),
|
||||
"label_ids": (
|
||||
"labels__id",
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
~Q(labels__id__isnull=True) & (Q(labels__deleted_at__isnull=True)),
|
||||
),
|
||||
"module_ids": (
|
||||
"issue_module__module_id",
|
||||
(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
& Q(issue_module__module__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -48,22 +48,14 @@ def string_date_filter(
|
||||
if term == "weeks":
|
||||
if subsequent == "after":
|
||||
if offset == "fromnow":
|
||||
issue_filter[f"{date_filter}__gte"] = now + timedelta(
|
||||
weeks=duration
|
||||
)
|
||||
issue_filter[f"{date_filter}__gte"] = now + timedelta(weeks=duration)
|
||||
else:
|
||||
issue_filter[f"{date_filter}__gte"] = now - timedelta(
|
||||
weeks=duration
|
||||
)
|
||||
issue_filter[f"{date_filter}__gte"] = now - timedelta(weeks=duration)
|
||||
else:
|
||||
if offset == "fromnow":
|
||||
issue_filter[f"{date_filter}__lte"] = now + timedelta(
|
||||
weeks=duration
|
||||
)
|
||||
issue_filter[f"{date_filter}__lte"] = now + timedelta(weeks=duration)
|
||||
else:
|
||||
issue_filter[f"{date_filter}__lte"] = now - timedelta(
|
||||
weeks=duration
|
||||
)
|
||||
issue_filter[f"{date_filter}__lte"] = now - timedelta(weeks=duration)
|
||||
|
||||
|
||||
def date_filter(issue_filter, date_term, queries):
|
||||
@@ -128,9 +120,7 @@ def filter_state_group(params, issue_filter, method, prefix=""):
|
||||
and len(params.get("state_group"))
|
||||
and params.get("state_group") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}state__group__in"] = params.get(
|
||||
"state_group"
|
||||
)
|
||||
issue_filter[f"{prefix}state__group__in"] = params.get("state_group")
|
||||
return issue_filter
|
||||
|
||||
|
||||
@@ -252,8 +242,8 @@ def filter_mentions(params, issue_filter, method, prefix=""):
|
||||
and len(params.get("mentions"))
|
||||
and params.get("mentions") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}issue_mention__mention__id__in"] = (
|
||||
params.get("mentions")
|
||||
issue_filter[f"{prefix}issue_mention__mention__id__in"] = params.get(
|
||||
"mentions"
|
||||
)
|
||||
return issue_filter
|
||||
|
||||
@@ -421,10 +411,7 @@ def filter_cycle(params, issue_filter, method, prefix=""):
|
||||
and len(params.get("cycle"))
|
||||
and params.get("cycle") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}issue_cycle__cycle_id__in"] = params.get(
|
||||
"cycle"
|
||||
)
|
||||
issue_filter[f"{prefix}issue_cycle__deleted_at__isnull"] = True
|
||||
issue_filter[f"{prefix}issue_cycle__cycle_id__in"] = params.get("cycle")
|
||||
return issue_filter
|
||||
|
||||
|
||||
@@ -447,7 +434,6 @@ def filter_module(params, issue_filter, method, prefix=""):
|
||||
issue_filter[f"{prefix}issue_module__module_id__in"] = params.get(
|
||||
"module"
|
||||
)
|
||||
issue_filter[f"{prefix}issue_module__deleted_at__isnull"] = True
|
||||
return issue_filter
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"turbo": "^2.1.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "18.2.48"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"name": "plane"
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
"next": "12.3.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.4",
|
||||
@@ -75,8 +76,8 @@
|
||||
"@plane/eslint-config": "*",
|
||||
"@plane/typescript-config": "*",
|
||||
"@types/node": "18.15.3",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsup": "^7.2.0",
|
||||
|
||||
@@ -72,8 +72,6 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerRect = useRef<DOMRect | null>(null);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const [hasErroredOnFirstLoad, setHasErroredOnFirstLoad] = useState(false);
|
||||
const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false);
|
||||
|
||||
const updateAttributesSafely = useCallback(
|
||||
(attributes: Partial<ImageAttributes>, errorMessage: string) => {
|
||||
@@ -147,9 +145,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
...prevSize,
|
||||
width: ensurePixelString(nodeWidth),
|
||||
height: ensurePixelString(nodeHeight),
|
||||
aspectRatio: nodeAspectRatio,
|
||||
}));
|
||||
}, [nodeWidth, nodeHeight, nodeAspectRatio]);
|
||||
}, [nodeWidth, nodeHeight]);
|
||||
|
||||
const handleResize = useCallback(
|
||||
(e: MouseEvent | TouchEvent) => {
|
||||
@@ -162,7 +159,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
|
||||
setSize((prevSize) => ({ ...prevSize, width: `${newWidth}px`, height: `${newHeight}px` }));
|
||||
},
|
||||
[size.aspectRatio]
|
||||
[size]
|
||||
);
|
||||
|
||||
const handleResizeEnd = useCallback(() => {
|
||||
@@ -185,15 +182,11 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
window.addEventListener("mousemove", handleResize);
|
||||
window.addEventListener("mouseup", handleResizeEnd);
|
||||
window.addEventListener("mouseleave", handleResizeEnd);
|
||||
window.addEventListener("touchmove", handleResize);
|
||||
window.addEventListener("touchend", handleResizeEnd);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleResize);
|
||||
window.removeEventListener("mouseup", handleResizeEnd);
|
||||
window.removeEventListener("mouseleave", handleResizeEnd);
|
||||
window.removeEventListener("touchmove", handleResize);
|
||||
window.removeEventListener("touchend", handleResizeEnd);
|
||||
};
|
||||
}
|
||||
}, [isResizing, handleResize, handleResizeEnd]);
|
||||
@@ -210,7 +203,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
|
||||
// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or)
|
||||
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
|
||||
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
|
||||
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete;
|
||||
// show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
const showImageUtils = remoteImageSrc && initialResizeComplete;
|
||||
// show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
@@ -238,26 +231,9 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
ref={imageRef}
|
||||
src={displayedImageSrc}
|
||||
onLoad={handleImageLoad}
|
||||
onError={async (e) => {
|
||||
// for old image extension this command doesn't exist or if the image failed to load for the first time
|
||||
if (!editor?.commands.restoreImage || hasTriedRestoringImageOnce) {
|
||||
setFailedToLoadImage(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setHasErroredOnFirstLoad(true);
|
||||
// this is a type error from tiptap, don't remove await until it's fixed
|
||||
await editor?.commands.restoreImage?.(node.attrs.src);
|
||||
imageRef.current.src = remoteImageSrc;
|
||||
} catch {
|
||||
// if the image failed to even restore, then show the error state
|
||||
setFailedToLoadImage(true);
|
||||
console.error("Error while loading image", e);
|
||||
} finally {
|
||||
setHasErroredOnFirstLoad(false);
|
||||
setHasTriedRestoringImageOnce(true);
|
||||
}
|
||||
onError={(e) => {
|
||||
console.error("Error loading image", e);
|
||||
setFailedToLoadImage(true);
|
||||
}}
|
||||
width={size.width}
|
||||
className={cn("image-component block rounded-md", {
|
||||
@@ -308,7 +284,6 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
}
|
||||
)}
|
||||
onMouseDown={handleResizeStart}
|
||||
onTouchStart={handleResizeStart}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { Editor, NodeViewWrapper } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image";
|
||||
|
||||
export type CustomImageComponentProps = {
|
||||
export type CustomImageNodeViewProps = {
|
||||
getPos: () => number;
|
||||
editor: Editor;
|
||||
node: NodeViewProps["node"] & {
|
||||
node: ProsemirrorNode & {
|
||||
attrs: ImageAttributes;
|
||||
};
|
||||
updateAttributes: (attrs: ImageAttributes) => void;
|
||||
updateAttributes: (attrs: Record<string, any>) => void;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type CustomImageNodeViewProps = NodeViewProps & CustomImageComponentProps;
|
||||
|
||||
export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
||||
const { getPos, editor, node, updateAttributes, selected } = props;
|
||||
const { src: remoteImageSrc } = node.attrs;
|
||||
|
||||
const [isUploaded, setIsUploaded] = useState(false);
|
||||
const [imageFromFileSystem, setImageFromFileSystem] = useState<string | undefined>(undefined);
|
||||
@@ -39,13 +37,14 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
||||
// the image is already uploaded if the image-component node has src attribute
|
||||
// and we need to remove the blob from our file system
|
||||
useEffect(() => {
|
||||
const remoteImageSrc = node.attrs.src;
|
||||
if (remoteImageSrc) {
|
||||
setIsUploaded(true);
|
||||
setImageFromFileSystem(undefined);
|
||||
} else {
|
||||
setIsUploaded(false);
|
||||
}
|
||||
}, [remoteImageSrc]);
|
||||
}, [node.attrs.src]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
@@ -56,7 +55,7 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
||||
editorContainer={editorContainer}
|
||||
editor={editor}
|
||||
// @ts-expect-error function not expected here, but will still work
|
||||
src={editor?.commands?.getImageSource?.(remoteImageSrc)}
|
||||
src={editor?.commands?.getImageSource?.(node.attrs.src)}
|
||||
getPos={getPos}
|
||||
node={node}
|
||||
setEditorContainer={setEditorContainer}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
// extensions
|
||||
import { type CustomImageComponentProps, getImageComponentImageFileMap } from "@/extensions/custom-image";
|
||||
import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image";
|
||||
|
||||
type CustomImageUploaderProps = CustomImageComponentProps & {
|
||||
maxFileSize: number;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
export const CustomImageUploader = (props: {
|
||||
editor: Editor;
|
||||
failedToLoadImage: boolean;
|
||||
getPos: () => number;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
maxFileSize: number;
|
||||
node: ProsemirrorNode & {
|
||||
attrs: ImageAttributes;
|
||||
};
|
||||
selected: boolean;
|
||||
setIsUploaded: (isUploaded: boolean) => void;
|
||||
};
|
||||
|
||||
export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
updateAttributes: (attrs: Record<string, any>) => void;
|
||||
}) => {
|
||||
const {
|
||||
editor,
|
||||
failedToLoadImage,
|
||||
@@ -29,8 +36,8 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
// refs
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const hasTriggeredFilePickerRef = useRef(false);
|
||||
const { id: imageEntityId } = node.attrs;
|
||||
// derived values
|
||||
const imageEntityId = node.attrs.id;
|
||||
const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Editor, mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// extensions
|
||||
import { CustomImageNode, ImageAttributes } from "@/extensions/custom-image";
|
||||
import { CustomImageNode } from "@/extensions/custom-image";
|
||||
// plugins
|
||||
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
|
||||
// types
|
||||
@@ -24,7 +22,6 @@ declare module "@tiptap/core" {
|
||||
imageComponent: {
|
||||
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
|
||||
uploadImage: (file: File) => () => Promise<string> | undefined;
|
||||
restoreImage: (src: string) => () => Promise<void>;
|
||||
getImageSource?: (path: string) => () => string;
|
||||
};
|
||||
}
|
||||
@@ -43,8 +40,8 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const {
|
||||
getAssetSrc,
|
||||
upload,
|
||||
delete: deleteImageFn,
|
||||
restore: restoreImageFn,
|
||||
delete: deleteImage,
|
||||
restore: restoreImage,
|
||||
validation: { maxFileSize },
|
||||
} = props;
|
||||
|
||||
@@ -88,6 +85,22 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
return ["image-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
onCreate(this) {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
|
||||
@@ -97,44 +110,17 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
|
||||
TrackImageDeletionPlugin(this.editor, deleteImage, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImage, this.name),
|
||||
];
|
||||
},
|
||||
|
||||
onCreate(this) {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImageFn(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
deletedImageSet: new Map<string, boolean>(),
|
||||
uploadInProgress: false,
|
||||
maxFileSize,
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: Node) {
|
||||
const attrs = node.attrs as ImageAttributes;
|
||||
const imageSource = state.esc(this?.editor?.commands?.getImageSource?.(attrs.src) || attrs.src);
|
||||
const imageWidth = state.esc(attrs.width?.toString());
|
||||
state.write(`<img src="${state.esc(imageSource)}" width="${imageWidth}" />`);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -193,9 +179,6 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const fileUrl = await upload(file);
|
||||
return fileUrl;
|
||||
},
|
||||
restoreImage: (src: string) => async () => {
|
||||
await restoreImageFn(src);
|
||||
},
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// components
|
||||
import { CustomImageNode, ImageAttributes, UploadImageExtensionStorage } from "@/extensions/custom-image";
|
||||
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
|
||||
// types
|
||||
import { TFileHandler } from "@/types";
|
||||
|
||||
@@ -54,15 +52,6 @@ export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAsset
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: Node) {
|
||||
const attrs = node.attrs as ImageAttributes;
|
||||
const imageSource = state.esc(this?.editor?.commands?.getImageSource?.(attrs.src) || attrs.src);
|
||||
const imageWidth = state.esc(attrs.width?.toString());
|
||||
state.write(`<img src="${state.esc(imageSource)}" width="${imageWidth}" />`);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
CustomLinkExtension,
|
||||
CustomMention,
|
||||
CustomQuoteExtension,
|
||||
CustomToggleHeadingExtension,
|
||||
CustomTypographyExtension,
|
||||
DropHandlerExtension,
|
||||
ImageExtension,
|
||||
@@ -140,10 +141,7 @@ export const CoreEditorExtensions = (args: TArguments) => {
|
||||
if (editor.storage.imageComponent.uploadInProgress) return "";
|
||||
|
||||
const shouldHidePlaceholder =
|
||||
editor.isActive("table") ||
|
||||
editor.isActive("codeBlock") ||
|
||||
editor.isActive("image") ||
|
||||
editor.isActive("imageComponent");
|
||||
editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image");
|
||||
|
||||
if (shouldHidePlaceholder) return "";
|
||||
|
||||
@@ -158,5 +156,6 @@ export const CoreEditorExtensions = (args: TArguments) => {
|
||||
}),
|
||||
CharacterCount,
|
||||
CustomColorExtension,
|
||||
CustomToggleHeadingExtension,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -11,9 +11,9 @@ import { CustomImageNode } from "@/extensions";
|
||||
|
||||
export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
const {
|
||||
delete: deleteImage,
|
||||
getAssetSrc,
|
||||
delete: deleteImageFn,
|
||||
restore: restoreImageFn,
|
||||
restore: restoreImage,
|
||||
validation: { maxFileSize },
|
||||
} = fileHandler;
|
||||
|
||||
@@ -24,11 +24,10 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
|
||||
TrackImageDeletionPlugin(this.editor, deleteImage, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImage, this.name),
|
||||
];
|
||||
},
|
||||
|
||||
@@ -36,14 +35,12 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImageFn(src);
|
||||
await restoreImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
@@ -68,9 +65,6 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -11,9 +11,6 @@ export const ImageExtensionWithoutProps = () =>
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,9 +18,6 @@ export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">)
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from "./issue-embed";
|
||||
export * from "./mentions";
|
||||
export * from "./slash-commands";
|
||||
export * from "./table";
|
||||
export * from "./toggle-heading";
|
||||
export * from "./typography";
|
||||
export * from "./core-without-props";
|
||||
export * from "./custom-code-inline";
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Editor, mergeAttributes } from "@tiptap/core";
|
||||
import Mention, { MentionOptions } from "@tiptap/extension-mention";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { ReactNodeViewRenderer, ReactRenderer } from "@tiptap/react";
|
||||
import tippy from "tippy.js";
|
||||
// extensions
|
||||
@@ -27,19 +25,8 @@ export const CustomMention = ({
|
||||
addStorage(this) {
|
||||
return {
|
||||
mentionsOpen: false,
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: Node) {
|
||||
const { attrs } = node;
|
||||
const label = `@${state.esc(attrs.label)}`;
|
||||
const originUrl = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
const safeRedirectionPath = state.esc(attrs.redirect_uri);
|
||||
const url = `${originUrl}${safeRedirectionPath}`;
|
||||
state.write(`[${label}](${url})`);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
id: {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
HeadingListExtension,
|
||||
CustomReadOnlyImageExtension,
|
||||
CustomColorExtension,
|
||||
CustomToggleHeadingReadOnlyExtension,
|
||||
} from "@/extensions";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
@@ -124,5 +125,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
|
||||
CharacterCount,
|
||||
CustomColorExtension,
|
||||
HeadingListExtension,
|
||||
CustomToggleHeadingReadOnlyExtension,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Heading6,
|
||||
ImageIcon,
|
||||
List,
|
||||
ListCollapse,
|
||||
ListOrdered,
|
||||
ListTodo,
|
||||
MinusSquare,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
toggleTextColor,
|
||||
toggleBackgroundColor,
|
||||
insertImage,
|
||||
insertToggleHeading,
|
||||
} from "@/helpers/editor-commands";
|
||||
// types
|
||||
import { CommandProps, ISlashCommandItem } from "@/types";
|
||||
@@ -49,7 +51,8 @@ export const getSlashCommandFilteredSections =
|
||||
({ query }: { query: string }): TSlashCommandSection[] => {
|
||||
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
|
||||
{
|
||||
key: "general",
|
||||
key: "basic",
|
||||
title: "Basic blocks",
|
||||
items: [
|
||||
{
|
||||
commandKey: "text",
|
||||
@@ -193,6 +196,39 @@ export const getSlashCommandFilteredSections =
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "advanced",
|
||||
title: "Advanced blocks",
|
||||
items: [
|
||||
{
|
||||
commandKey: "toggle-heading",
|
||||
key: "toggle-heading-1",
|
||||
title: "Toggle heading 1",
|
||||
icon: <ListCollapse className="size-3.5" />,
|
||||
description: "Insert toggle heading 1",
|
||||
searchTerms: ["toggle", "heading", "collapse", "disclosure", "accordion"],
|
||||
command: ({ editor, range }) => insertToggleHeading(1, editor, range),
|
||||
},
|
||||
{
|
||||
commandKey: "toggle-heading",
|
||||
key: "toggle-heading-2",
|
||||
title: "Toggle heading 2",
|
||||
icon: <ListCollapse className="size-3.5" />,
|
||||
description: "Insert toggle heading 2",
|
||||
searchTerms: ["toggle", "heading", "collapse", "disclosure", "accordion"],
|
||||
command: ({ editor, range }) => insertToggleHeading(2, editor, range),
|
||||
},
|
||||
{
|
||||
commandKey: "toggle-heading",
|
||||
key: "toggle-heading-3",
|
||||
title: "Toggle heading 3",
|
||||
icon: <ListCollapse className="size-3.5" />,
|
||||
description: "Insert toggle heading 3",
|
||||
searchTerms: ["toggle", "heading", "collapse", "disclosure", "accordion"],
|
||||
command: ({ editor, range }) => insertToggleHeading(3, editor, range),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "text-color",
|
||||
title: "Colors",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
// types
|
||||
import { TToggleHeadingBlockAttributes } from "./types";
|
||||
import { cn } from "@/helpers/common";
|
||||
|
||||
type Props = NodeViewProps & {
|
||||
node: NodeViewProps["node"] & {
|
||||
attrs: TToggleHeadingBlockAttributes;
|
||||
};
|
||||
};
|
||||
|
||||
export const CustomToggleHeadingBlock: React.FC<Props> = (props) => {
|
||||
const { node, updateAttributes } = props;
|
||||
// derived values
|
||||
const headingLevel = Number(node.attrs["data-heading-level"] ?? 1);
|
||||
const isToggleOpen = node.attrs["data-toggle-status"] === "open";
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="editor-toggle-heading-component flex items-start gap-1 my-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateAttributes({
|
||||
"data-toggle-status": isToggleOpen ? "close" : "open",
|
||||
});
|
||||
}}
|
||||
className="flex-shrink-0 size-5 grid place-items-center rounded hover:bg-custom-background-80 transition-colors"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("size-3.5 transition-all", {
|
||||
"rotate-90": isToggleOpen,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
<NodeViewContent as="div" className="w-full break-words" />
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { Node as NodeType } from "@tiptap/pm/model";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
|
||||
// Extend Tiptap's Commands interface
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
toggleHeadingComponent: {
|
||||
insertToggleHeading: ({ headingLevel }: { headingLevel: number }) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomToggleHeadingExtensionConfig = Node.create({
|
||||
name: "toggleHeadingComponent",
|
||||
group: "block",
|
||||
content: "block+",
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
"data-heading-level": {
|
||||
default: 1,
|
||||
},
|
||||
"data-background-color": {
|
||||
default: null,
|
||||
},
|
||||
"data-toggle-status": {
|
||||
default: "close",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: NodeType) {},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: "toggle-heading-component" }];
|
||||
},
|
||||
|
||||
// Render HTML for the callout node
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["toggle-heading-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// components
|
||||
import { CustomToggleHeadingBlock } from "./block";
|
||||
// config
|
||||
import { CustomToggleHeadingExtensionConfig } from "./extension-config";
|
||||
// utils
|
||||
import { DEFAULT_TOGGLE_HEADING_BLOCK_ATTRIBUTES } from "./utils";
|
||||
|
||||
export const CustomToggleHeadingExtension = CustomToggleHeadingExtensionConfig.extend({
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
insertToggleHeading:
|
||||
({ headingLevel }) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent({
|
||||
type: this.name,
|
||||
content: [
|
||||
{
|
||||
type: "heading",
|
||||
attrs: {
|
||||
level: headingLevel,
|
||||
},
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
"data-heading-level": headingLevel ?? 1,
|
||||
"data-toggle-status": DEFAULT_TOGGLE_HEADING_BLOCK_ATTRIBUTES["data-toggle-status"],
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomToggleHeadingBlock);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./extension";
|
||||
export * from "./read-only-extension";
|
||||
@@ -0,0 +1,3 @@
|
||||
import { CustomToggleHeadingExtensionConfig } from "./extension-config";
|
||||
|
||||
export const CustomToggleHeadingReadOnlyExtension = CustomToggleHeadingExtensionConfig;
|
||||
@@ -0,0 +1,5 @@
|
||||
export type TToggleHeadingBlockAttributes = {
|
||||
"data-heading-level": number | undefined;
|
||||
"data-background-color": string | undefined;
|
||||
"data-toggle-status": "open" | "close" | undefined;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
// types
|
||||
import { TToggleHeadingBlockAttributes } from "./types";
|
||||
|
||||
export const DEFAULT_TOGGLE_HEADING_BLOCK_ATTRIBUTES: TToggleHeadingBlockAttributes = {
|
||||
"data-heading-level": undefined,
|
||||
"data-background-color": undefined,
|
||||
"data-toggle-status": "close",
|
||||
};
|
||||
@@ -180,3 +180,8 @@ export const toggleBackgroundColor = (color: string | undefined, editor: Editor,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const insertToggleHeading = (headingLevel: number, editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).insertToggleHeading({ headingLevel }).run();
|
||||
else editor.chain().focus().insertToggleHeading({ headingLevel }).run();
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||
".issue-embed",
|
||||
".image-component",
|
||||
".image-upload-component",
|
||||
".editor-toggle-heading-component",
|
||||
].join(", ");
|
||||
|
||||
for (const elem of elements) {
|
||||
|
||||
@@ -25,9 +25,6 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor
|
||||
if (node.type.name !== nodeType) return;
|
||||
if (pos < 0 || pos > newState.doc.content.size) return;
|
||||
if (oldImageSources.has(node.attrs.src)) return;
|
||||
// if the src is just a id (private bucket), then we don't need to handle restore from here but
|
||||
// only while it fails to load
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
addedImages.push(node as ImageNode);
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ export type TEditorCommands =
|
||||
| "divider"
|
||||
| "issue-embed"
|
||||
| "text-color"
|
||||
| "background-color";
|
||||
| "background-color"
|
||||
| "toggle-heading";
|
||||
|
||||
export type TColorEditorCommands = Extract<TEditorCommands, "text-color" | "background-color">;
|
||||
export type TNonColorEditorCommands = Exclude<TEditorCommands, "text-color" | "background-color">;
|
||||
|
||||
@@ -316,7 +316,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
|
||||
/* tailwind typography */
|
||||
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 2rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
margin-bottom: 4px;
|
||||
font-size: var(--font-size-h1);
|
||||
line-height: var(--line-height-h1);
|
||||
@@ -324,7 +327,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1.4rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1.4rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h2);
|
||||
line-height: var(--line-height-h2);
|
||||
@@ -332,7 +338,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h3);
|
||||
line-height: var(--line-height-h3);
|
||||
@@ -340,7 +349,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h4);
|
||||
line-height: var(--line-height-h4);
|
||||
@@ -348,7 +360,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h5);
|
||||
line-height: var(--line-height-h5);
|
||||
@@ -356,7 +371,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h6);
|
||||
line-height: var(--line-height-h6);
|
||||
@@ -364,7 +382,14 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 0.25rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
padding: 3px 0;
|
||||
font-size: var(--font-size-regular);
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^18.3.11",
|
||||
"typescript": "^5.3.3",
|
||||
"@types/react": "^18.3.5",
|
||||
"typescript": "^5.6.2",
|
||||
"tsup": "^7.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
Vendored
-13
@@ -96,16 +96,3 @@ export type TInboxIssuePaginationInfo = TPaginationInfo & {
|
||||
export type TInboxIssueWithPagination = TInboxIssuePaginationInfo & {
|
||||
results: TInboxIssue[];
|
||||
};
|
||||
|
||||
export type TInboxForm = {
|
||||
anchor: string;
|
||||
id: string;
|
||||
is_disabled: boolean;
|
||||
};
|
||||
|
||||
export type TInboxIssueForm = {
|
||||
name: string;
|
||||
description: string;
|
||||
username: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
"postcss": "postcss styles/globals.css -o styles/output.css --watch",
|
||||
"lint": "eslint src --ext .ts,.tsx"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.10",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
@@ -37,6 +33,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.379.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"sonner": "^1.4.41",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
@@ -56,14 +53,15 @@
|
||||
"@storybook/test": "^8.1.1",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.5.2",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-color": "^3.0.9",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"classnames": "^2.3.2",
|
||||
"@plane/eslint-config": "*",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"postcss-nested": "^6.0.1",
|
||||
"react": "^18.2.0",
|
||||
"storybook": "^8.1.1",
|
||||
"tailwind-config-custom": "*",
|
||||
"tailwindcss": "^3.4.3",
|
||||
|
||||
@@ -16,7 +16,7 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
|
||||
const { priority, className = "", containerClassName = "", size = 14, withContainer = false } = props;
|
||||
|
||||
const priorityClasses = {
|
||||
urgent: "bg-red-600/20 text-red-600 border-red-600",
|
||||
urgent: "bg-red-600 text-red-500 border-red-600",
|
||||
high: "bg-orange-500/20 text-orange-500 border-orange-500",
|
||||
medium: "bg-yellow-500/20 text-yellow-500 border-yellow-500",
|
||||
low: "bg-custom-primary-100/20 text-custom-primary-100 border-custom-primary-100",
|
||||
@@ -49,6 +49,8 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
|
||||
size={size}
|
||||
className={cn(
|
||||
{
|
||||
"text-white": priority === "urgent",
|
||||
// centre align the icons
|
||||
"translate-x-[0.0625rem]": priority === "high",
|
||||
"translate-x-0.5": priority === "medium",
|
||||
"translate-x-1": priority === "low",
|
||||
|
||||
@@ -18,7 +18,6 @@ export const Popover = (props: TPopover) => {
|
||||
panelClassName = "",
|
||||
children,
|
||||
popoverButtonRef,
|
||||
buttonRefClassName = "",
|
||||
} = props;
|
||||
// states
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -39,7 +38,7 @@ export const Popover = (props: TPopover) => {
|
||||
|
||||
return (
|
||||
<HeadlessReactPopover className={cn("relative flex h-full w-full items-center justify-center", popoverClassName)}>
|
||||
<div ref={setReferenceElement} className={cn("w-full", buttonRefClassName)}>
|
||||
<div ref={setReferenceElement} className="w-full">
|
||||
<HeadlessReactPopover.Button
|
||||
ref={popoverButtonRef as Ref<HTMLButtonElement>}
|
||||
className={cn(
|
||||
|
||||
@@ -5,7 +5,6 @@ export type TPopoverButtonDefaultOptions = {
|
||||
// button and button styling
|
||||
button?: ReactNode;
|
||||
buttonClassName?: string;
|
||||
buttonRefClassName?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ export class FileUploadService extends APIService {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
cancelToken: this.cancelSource.token,
|
||||
withCredentials: false,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TFileHandler } from "@plane/editor";
|
||||
import { MAX_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
@@ -33,7 +34,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
if (checkURLValidity(path)) {
|
||||
return path;
|
||||
} else {
|
||||
return getEditorAssetSrc(anchor, path) ?? "";
|
||||
@@ -41,14 +42,14 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
},
|
||||
upload: uploadFile,
|
||||
delete: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
if (checkURLValidity(src)) {
|
||||
await fileService.deleteOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? "");
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
if (checkURLValidity(src)) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(anchor, src);
|
||||
@@ -72,7 +73,7 @@ export const getReadOnlyEditorFileHandlers = (
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
if (checkURLValidity(path)) {
|
||||
return path;
|
||||
} else {
|
||||
return getEditorAssetSrc(anchor, path) ?? "";
|
||||
|
||||
+2
-2
@@ -54,8 +54,8 @@
|
||||
"@types/lodash": "^4.17.1",
|
||||
"@types/node": "18.14.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@types/zxcvbn": "^4.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 135 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 190 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
// components
|
||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||
import { ProjectInboxHeader } from "@/plane-web/components/projects/settings/intake";
|
||||
import { ProjectInboxHeader } from "./header";
|
||||
|
||||
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
// plane ui
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// components
|
||||
import { ProIcon } from "@/components/common";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
export const IssueEmbedUpgradeCard: React.FC<any> = (props) => (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full h-20 bg-custom-background-80 rounded-md border-[0.5px] border-custom-border-200 shadow-custom-shadow-2xs",
|
||||
{
|
||||
"border-2": props.selected,
|
||||
}
|
||||
)}
|
||||
className={`${
|
||||
props.selected ? "border-custom-primary-200 border-[2px]" : ""
|
||||
} w-full h-[100px] cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 shadow-custom-shadow-2xs`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-5 mt-2.5 pl-4 pr-5 py-3 w-full max-md:max-w-full max-md:flex-wrap rounded-md">
|
||||
<div className="flex items-center gap-4">
|
||||
<ProIcon className="flex-shrink-0 size-4" />
|
||||
<p className="text-custom-text !text-base">
|
||||
Embed and access issues in pages seamlessly, upgrade to Plane Pro now.
|
||||
</p>
|
||||
<h5 className="h-[20%] text-xs text-custom-text-300 p-2">
|
||||
{props.node?.attrs?.project_identifier}-{props?.node?.attrs?.sequence_id}
|
||||
</h5>
|
||||
<div className="relative h-[71%]">
|
||||
<div className="h-full backdrop-filter backdrop-blur-[30px] bg-custom-background-80 bg-opacity-30 flex items-center w-full justify-between gap-5 mt-2.5 pl-4 pr-5 py-3 max-md:max-w-full max-md:flex-wrap relative">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="rounded">
|
||||
<ProIcon className="m-2 w-4 h-4" />
|
||||
</div>
|
||||
<div className="text-custom-text text-sm">
|
||||
Embed and access issues in pages seamlessly, upgrade to plane pro now.
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://plane.so/pricing" target="_blank" rel="noopener noreferrer">
|
||||
<Button>Upgrade</Button>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href="https://plane.so/pro"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(getButtonStyling("primary", "md"), "no-underline")}
|
||||
>
|
||||
Upgrade
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./header";
|
||||
@@ -1,23 +1,16 @@
|
||||
import { ReactNode } from "react";
|
||||
import { FileText, Layers, Timer } from "lucide-react";
|
||||
import { IProject } from "@plane/types";
|
||||
import { ContrastIcon, DiceIcon, Intake } from "@plane/ui";
|
||||
|
||||
export type TProperties = {
|
||||
property: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
isPro: boolean;
|
||||
isEnabled: boolean;
|
||||
renderChildren?: (
|
||||
currentProjectDetails: IProject,
|
||||
isAdmin: boolean,
|
||||
handleSubmit: (featureKey: string, featureProperty: string) => Promise<void>
|
||||
) => ReactNode;
|
||||
};
|
||||
export type TFeatureList = {
|
||||
[key: string]: TProperties;
|
||||
[key: string]: {
|
||||
property: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
isPro: boolean;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type TProjectFeatures = {
|
||||
|
||||
@@ -63,7 +63,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
||||
|
||||
const priorityClasses = {
|
||||
urgent: "bg-red-600/10 text-red-600 border-red-600 px-1",
|
||||
urgent: "bg-red-500/20 text-red-950 border-red-500",
|
||||
high: "bg-orange-500/20 text-orange-950 border-orange-500",
|
||||
medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500",
|
||||
low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100",
|
||||
@@ -88,7 +88,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": hideText,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-600/10 border-red-600": priority === "urgent" && hideText && highlightUrgent,
|
||||
"bg-red-600 border-red-600": priority === "urgent" && hideText && highlightUrgent,
|
||||
},
|
||||
className
|
||||
)}
|
||||
@@ -98,8 +98,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
<div
|
||||
className={cn({
|
||||
// highlight just the icon if text is visible and priority is urgent
|
||||
"bg-red-600/20 p-0.5 rounded border border-red-600":
|
||||
priority === "urgent" && !hideText && highlightUrgent,
|
||||
"bg-red-600 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
|
||||
})}
|
||||
>
|
||||
<PriorityIcon
|
||||
@@ -113,6 +112,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
"translate-x-0.5": hideText && priority === "medium",
|
||||
"translate-x-1": hideText && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
"text-white": priority === "urgent" && highlightUrgent,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
||||
|
||||
const priorityClasses = {
|
||||
urgent: "bg-red-600/20 text-red-600",
|
||||
urgent: "bg-red-500/20 text-red-950",
|
||||
high: "bg-orange-500/20 text-orange-950",
|
||||
medium: "bg-yellow-500/20 text-yellow-950",
|
||||
low: "bg-blue-500/20 text-blue-950",
|
||||
@@ -170,7 +170,7 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": hideText,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-600/10 border-red-600": priority === "urgent" && hideText && highlightUrgent,
|
||||
"bg-red-600 border-red-600": priority === "urgent" && hideText && highlightUrgent,
|
||||
},
|
||||
className
|
||||
)}
|
||||
@@ -180,8 +180,7 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
<div
|
||||
className={cn({
|
||||
// highlight just the icon if text is visible and priority is urgent
|
||||
"bg-red-600/20 p-0.5 rounded border border-red-600":
|
||||
priority === "urgent" && !hideText && highlightUrgent,
|
||||
"bg-red-600 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
|
||||
})}
|
||||
>
|
||||
<PriorityIcon
|
||||
@@ -195,6 +194,7 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
"translate-x-0.5": hideText && priority === "medium",
|
||||
"translate-x-1": hideText && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
"text-white": priority === "urgent" && highlightUrgent,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -253,7 +253,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": hideText,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-600/10 border-red-600": priority === "urgent" && hideText && highlightUrgent,
|
||||
"bg-red-600 border-red-600": priority === "urgent" && hideText && highlightUrgent,
|
||||
"bg-custom-background-80": isActive,
|
||||
},
|
||||
className
|
||||
@@ -264,8 +264,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
<div
|
||||
className={cn({
|
||||
// highlight just the icon if text is visible and priority is urgent
|
||||
"bg-red-600/20 p-0.5 rounded border border-red-600":
|
||||
priority === "urgent" && !hideText && highlightUrgent,
|
||||
"bg-red-600 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
|
||||
})}
|
||||
>
|
||||
<PriorityIcon
|
||||
@@ -279,6 +278,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
"translate-x-0.5": hideText && priority === "medium",
|
||||
"translate-x-1": hideText && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
"text-white": priority === "urgent" && highlightUrgent,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { MinusCircle } from "lucide-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
// component
|
||||
// ui
|
||||
import { ControlLink, CustomMenu } from "@plane/ui";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssues, useProjectState } from "@/hooks/store";
|
||||
import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// types
|
||||
@@ -30,8 +29,6 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
|
||||
// hooks
|
||||
const { issueMap } = useIssues();
|
||||
const { getProjectStates } = useProjectState();
|
||||
const { handleRedirection } = useIssuePeekOverviewRedirection();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const parentIssue = issueMap?.[issue.parent_id || ""] || undefined;
|
||||
|
||||
@@ -45,10 +42,7 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 flex w-min items-center gap-3 whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-80 px-2.5 py-1 text-xs">
|
||||
<ControlLink
|
||||
href={`/${workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`}
|
||||
onClick={() => handleRedirection(workspaceSlug, parentIssue, isMobile)}
|
||||
>
|
||||
<Link href={`/${workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`} target="_blank">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="block h-2 w-2 rounded-full" style={{ backgroundColor: stateColor }} />
|
||||
@@ -62,7 +56,7 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
|
||||
</div>
|
||||
<span className="truncate text-custom-text-100">{(parentIssue?.name ?? "").substring(0, 50)}</span>
|
||||
</div>
|
||||
</ControlLink>
|
||||
</Link>
|
||||
|
||||
<CustomMenu ellipsis optionsClassName="p-1.5">
|
||||
<div className="border-b border-custom-border-300 text-xs font-medium text-custom-text-200">
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
// types
|
||||
import type { TBaseIssue, TIssue } from "@plane/types";
|
||||
import type { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { CreateIssueToastActionItems, IssuesModalProps } from "@/components/issues";
|
||||
@@ -252,22 +252,6 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
if (isDraft) await draftIssues.updateIssue(workspaceSlug.toString(), data.id, payload);
|
||||
else if (updateIssue) await updateIssue(payload.project_id, data.id, payload);
|
||||
|
||||
// check if we should add issue to cycle/module
|
||||
if (
|
||||
payload.cycle_id &&
|
||||
payload.cycle_id !== "" &&
|
||||
(payload.cycle_id !== cycleId || storeType !== EIssuesStoreType.CYCLE)
|
||||
) {
|
||||
await addIssueToCycle(data as TBaseIssue, payload.cycle_id);
|
||||
}
|
||||
if (
|
||||
payload.module_ids &&
|
||||
payload.module_ids.length > 0 &&
|
||||
(!payload.module_ids.includes(moduleId?.toString()) || storeType !== EIssuesStoreType.MODULE)
|
||||
) {
|
||||
await addIssueToModule(data as TBaseIssue, payload.module_ids);
|
||||
}
|
||||
|
||||
// add other property values
|
||||
await handleCreateUpdatePropertyValues({
|
||||
issueId: data.id,
|
||||
|
||||
@@ -177,6 +177,7 @@ export const DraftIssueBlock: FC<Props> = observer((props) => {
|
||||
updateIssue={async (projectId, issueId, data) => {
|
||||
await updateIssue(workspaceSlug, issueId, data);
|
||||
}}
|
||||
activeLayout="List"
|
||||
/>
|
||||
<div
|
||||
className={cn("hidden", {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import xor from "lodash/xor";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
// icons
|
||||
import { CalendarCheck2, CalendarClock } from "lucide-react";
|
||||
// types
|
||||
import { TIssuePriorities, TWorkspaceDraftIssue } from "@plane/types";
|
||||
import { TIssue, TIssuePriorities, TWorkspaceDraftIssue } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
DateDropdown,
|
||||
@@ -17,11 +18,20 @@ import {
|
||||
CycleDropdown,
|
||||
StateDropdown,
|
||||
} from "@/components/dropdowns";
|
||||
// constants
|
||||
import { ISSUE_UPDATED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useLabel, useProjectState, useProject, useProjectEstimates, useWorkspaceDraftIssues } from "@/hooks/store";
|
||||
import {
|
||||
useEventTracker,
|
||||
useLabel,
|
||||
useProjectState,
|
||||
useProject,
|
||||
useProjectEstimates,
|
||||
useWorkspaceDraftIssues,
|
||||
} from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local components
|
||||
import { IssuePropertyLabels } from "../issue-layouts";
|
||||
@@ -32,13 +42,15 @@ export interface IIssueProperties {
|
||||
| ((projectId: string | null, issueId: string, data: Partial<TWorkspaceDraftIssue>) => Promise<void>)
|
||||
| undefined;
|
||||
className: string;
|
||||
activeLayout: string;
|
||||
}
|
||||
|
||||
export const DraftIssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
const { issue, updateIssue, className } = props;
|
||||
const { issue, updateIssue, activeLayout, className } = props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { labelMap } = useLabel();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { addCycleToIssue, addModulesToIssue } = useWorkspaceDraftIssues();
|
||||
const { areEstimateEnabledByProjectId } = useProjectEstimates();
|
||||
const { getStateById } = useProjectState();
|
||||
@@ -47,12 +59,19 @@ export const DraftIssueProperties: React.FC<IIssueProperties> = observer((props)
|
||||
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const currentLayout = `${activeLayout} layout`;
|
||||
// derived values
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
|
||||
const issueOperations = useMemo(
|
||||
() => ({
|
||||
updateIssueModules: async (moduleIds: string[]) => {
|
||||
addModulesToIssue: async (moduleIds: string[]) => {
|
||||
if (!workspaceSlug || !issue.id) return;
|
||||
await addModulesToIssue(workspaceSlug.toString(), issue.id, moduleIds);
|
||||
},
|
||||
removeModulesFromIssue: async (moduleIds: string[]) => {
|
||||
if (!workspaceSlug || !issue.id) return;
|
||||
await addModulesToIssue(workspaceSlug.toString(), issue.id, moduleIds);
|
||||
},
|
||||
@@ -84,9 +103,17 @@ export const DraftIssueProperties: React.FC<IIssueProperties> = observer((props)
|
||||
const handleModule = useCallback(
|
||||
(moduleIds: string[] | null) => {
|
||||
if (!issue || !issue.module_ids || !moduleIds) return;
|
||||
issueOperations.updateIssueModules(moduleIds);
|
||||
|
||||
const updatedModuleIds = xor(issue.module_ids, moduleIds);
|
||||
const modulesToAdd: string[] = [];
|
||||
const modulesToRemove: string[] = [];
|
||||
for (const moduleId of updatedModuleIds)
|
||||
if (issue.module_ids.includes(moduleId)) modulesToRemove.push(moduleId);
|
||||
else modulesToAdd.push(moduleId);
|
||||
if (modulesToAdd.length > 0) issueOperations.addModulesToIssue(modulesToAdd);
|
||||
if (modulesToRemove.length > 0) issueOperations.removeModulesFromIssue(modulesToRemove);
|
||||
},
|
||||
[issueOperations, issue]
|
||||
[issueOperations, currentLayout, pathname, issue]
|
||||
);
|
||||
|
||||
const handleCycle = useCallback(
|
||||
@@ -95,7 +122,7 @@ export const DraftIssueProperties: React.FC<IIssueProperties> = observer((props)
|
||||
if (cycleId) issueOperations.addIssueToCycle?.(cycleId);
|
||||
else issueOperations.removeIssueFromCycle?.();
|
||||
},
|
||||
[issue, issueOperations]
|
||||
[issue, issueOperations, currentLayout, pathname]
|
||||
);
|
||||
|
||||
const handleStartDate = (date: Date | null) =>
|
||||
|
||||
@@ -7,21 +7,12 @@ import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||
import { Info, SquareUser } from "lucide-react";
|
||||
// ui
|
||||
import { IModule } from "@plane/types";
|
||||
import {
|
||||
Card,
|
||||
FavoriteStar,
|
||||
LayersIcon,
|
||||
LinearProgressIndicator,
|
||||
TOAST_TYPE,
|
||||
Tooltip,
|
||||
setPromiseToast,
|
||||
setToast,
|
||||
} from "@plane/ui";
|
||||
import { Card, FavoriteStar, LayersIcon, LinearProgressIndicator, TOAST_TYPE, Tooltip, setPromiseToast, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
|
||||
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
|
||||
// constants
|
||||
import { PROGRESS_STATE_GROUPS_DETAILS } from "@/constants/common";
|
||||
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
|
||||
@@ -30,10 +21,11 @@ import { MODULE_STATUS } from "@/constants/module";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { generateQueryParams } from "@/helpers/router.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useModule, useUserPermissions } from "@/hooks/store";
|
||||
import { useEventTracker, useMember, useModule, useProjectEstimates, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web constants
|
||||
import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
type Props = {
|
||||
@@ -54,6 +46,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule();
|
||||
const { getUserDetails } = useMember();
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
||||
|
||||
// derived values
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
@@ -64,6 +57,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
const isDisabled = !isEditingAllowed || !!moduleDetails?.archived_at;
|
||||
const renderIcon = Boolean(moduleDetails?.start_date) || Boolean(moduleDetails?.target_date);
|
||||
|
||||
|
||||
const { isMobile } = usePlatformOS();
|
||||
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
@@ -162,14 +156,29 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
if (!moduleDetails) return null;
|
||||
|
||||
const moduleTotalIssues =
|
||||
moduleDetails.backlog_issues +
|
||||
moduleDetails.unstarted_issues +
|
||||
moduleDetails.started_issues +
|
||||
moduleDetails.completed_issues +
|
||||
moduleDetails.cancelled_issues;
|
||||
/**
|
||||
* NOTE: This completion percentage calculation is based on the total issues count.
|
||||
* when estimates are available and estimate type is points, we should consider the estimate point count
|
||||
* when estimates are available and estimate type is not points, then by default we consider the issue count
|
||||
*/
|
||||
const isEstimateEnabled =
|
||||
projectId &&
|
||||
currentActiveEstimateId &&
|
||||
areEstimateEnabledByProjectId(projectId.toString()) &&
|
||||
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS;
|
||||
|
||||
const moduleTotalIssues = isEstimateEnabled
|
||||
? moduleDetails?.total_estimate_points || 0
|
||||
: moduleDetails.backlog_issues +
|
||||
moduleDetails.unstarted_issues +
|
||||
moduleDetails.started_issues +
|
||||
moduleDetails.completed_issues +
|
||||
moduleDetails.cancelled_issues;
|
||||
|
||||
const moduleCompletedIssues = isEstimateEnabled
|
||||
? moduleDetails?.completed_estimate_points || 0
|
||||
: moduleDetails.completed_issues;
|
||||
|
||||
const moduleCompletedIssues = moduleDetails.completed_issues;
|
||||
|
||||
// const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
|
||||
|
||||
@@ -177,11 +186,11 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
const issueCount = module
|
||||
? !moduleTotalIssues || moduleTotalIssues === 0
|
||||
? `0 Issue`
|
||||
? `0 ${isEstimateEnabled ? `Point` : `Issue`}`
|
||||
: moduleTotalIssues === moduleCompletedIssues
|
||||
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? `s` : ``}`
|
||||
: `${moduleCompletedIssues}/${moduleTotalIssues} Issues`
|
||||
: `0 Issue`;
|
||||
: `${moduleCompletedIssues}/${moduleTotalIssues} ${isEstimateEnabled ? `Points` : `Issues`}`
|
||||
: `0 ${isEstimateEnabled ? `Point` : `Issue`}`;
|
||||
|
||||
const moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;
|
||||
|
||||
@@ -204,9 +213,9 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-2" onClick={handleEventPropagation}>
|
||||
{moduleStatus && (
|
||||
<ModuleStatusDropdown
|
||||
isDisabled={isDisabled}
|
||||
moduleDetails={moduleDetails}
|
||||
handleModuleDetailsChange={handleModuleDetailsChange}
|
||||
isDisabled={isDisabled}
|
||||
moduleDetails={moduleDetails}
|
||||
handleModuleDetailsChange={handleModuleDetailsChange}
|
||||
/>
|
||||
)}
|
||||
<button onClick={openModuleOverview}>
|
||||
@@ -243,9 +252,9 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
onSelect={(val) => {
|
||||
handleModuleDetailsChange({
|
||||
start_date: val?.from ? renderFormattedPayloadDate(val.from) : null,
|
||||
target_date: val?.to ? renderFormattedPayloadDate(val.to) : null,
|
||||
});
|
||||
start_date: (val?.from ? renderFormattedPayloadDate(val.from) : null),
|
||||
target_date: (val?.to ? renderFormattedPayloadDate(val.to) : null)
|
||||
})
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
@@ -279,4 +288,4 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -13,9 +13,11 @@ import { ModuleListItemAction, ModuleQuickActions } from "@/components/modules";
|
||||
// helpers
|
||||
import { generateQueryParams } from "@/helpers/router.helper";
|
||||
// hooks
|
||||
import { useModule } from "@/hooks/store";
|
||||
import { useModule, useProjectEstimates } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web constants
|
||||
import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
||||
|
||||
type Props = {
|
||||
moduleId: string;
|
||||
@@ -33,14 +35,27 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
// store hooks
|
||||
const { getModuleById } = useModule();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
||||
|
||||
// derived values
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
|
||||
if (!moduleDetails) return null;
|
||||
|
||||
const completionPercentage =
|
||||
((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
|
||||
/**
|
||||
* NOTE: This completion percentage calculation is based on the total issues count.
|
||||
* when estimates are available and estimate type is points, we should consider the estimate point count
|
||||
* when estimates are available and estimate type is not points, then by default we consider the issue count
|
||||
*/
|
||||
const isEstimateEnabled =
|
||||
projectId &&
|
||||
currentActiveEstimateId &&
|
||||
areEstimateEnabledByProjectId(projectId?.toString()) &&
|
||||
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS;
|
||||
|
||||
const completionPercentage = isEstimateEnabled
|
||||
? ((moduleDetails?.completed_estimate_points || 0) / (moduleDetails?.total_estimate_points || 0)) * 100
|
||||
: ((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
|
||||
|
||||
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
|
||||
|
||||
|
||||
@@ -70,40 +70,31 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
||||
return (
|
||||
<div
|
||||
key={featureItemKey}
|
||||
className="gap-x-8 gap-y-2 border-b border-custom-border-100 bg-custom-background-100 pb-2 pt-4"
|
||||
className="flex items-center justify-between gap-x-8 gap-y-2 border-b border-custom-border-100 bg-custom-background-100 pb-2 pt-4 last:border-b-0"
|
||||
>
|
||||
<div key={featureItemKey} className="flex items-center justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center rounded bg-custom-background-90 p-3">
|
||||
{featureItem.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium leading-5">{featureItem.title}</h4>
|
||||
{featureItem.isPro && (
|
||||
<Tooltip tooltipContent="Pro feature" position="top">
|
||||
<UpgradeBadge />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm leading-5 tracking-tight text-custom-text-300">
|
||||
{featureItem.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center rounded bg-custom-background-90 p-3">
|
||||
{featureItem.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium leading-5">{featureItem.title}</h4>
|
||||
{featureItem.isPro && (
|
||||
<Tooltip tooltipContent="Pro feature" position="top">
|
||||
<UpgradeBadge />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm leading-5 tracking-tight text-custom-text-300">{featureItem.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToggleSwitch
|
||||
value={Boolean(currentProjectDetails?.[featureItem.property as keyof IProject])}
|
||||
onChange={() => handleSubmit(featureItemKey, featureItem.property)}
|
||||
disabled={!featureItem.isEnabled || !isAdmin}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="pl-14">
|
||||
{currentProjectDetails?.[featureItem.property as keyof IProject] &&
|
||||
featureItem.renderChildren &&
|
||||
featureItem.renderChildren(currentProjectDetails, isAdmin, handleSubmit)}
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
value={Boolean(currentProjectDetails?.[featureItem.property as keyof IProject])}
|
||||
onChange={() => handleSubmit(featureItemKey, featureItem.property)}
|
||||
disabled={!featureItem.isEnabled || !isAdmin}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -16,7 +16,6 @@ export class FileUploadService extends APIService {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
cancelToken: this.cancelSource.token,
|
||||
withCredentials: false,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// types
|
||||
import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm } from "@plane/types";
|
||||
import type { TInboxIssue, TIssue, TInboxIssueWithPagination } from "@plane/types";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "@/services/api.service";
|
||||
// helpers
|
||||
@@ -75,30 +75,4 @@ export class InboxIssueService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrievePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updatePublishForm(workspaceSlug: string, projectId: string, is_disabled: boolean): Promise<TInboxIssue> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake/`, {
|
||||
is_disabled,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async regeneratePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxIssue> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake-regenerate/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
TInboxIssueSorting,
|
||||
TInboxIssuePaginationInfo,
|
||||
TInboxIssueSortingOrderByQueryParam,
|
||||
TInboxForm,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { EInboxIssueCurrentTab, EInboxIssueStatus, EPastDurationFilters, getCustomDates } from "@/helpers/inbox.helper";
|
||||
@@ -40,7 +39,6 @@ export interface IProjectInboxStore {
|
||||
inboxIssuePaginationInfo: TInboxIssuePaginationInfo | undefined;
|
||||
inboxIssues: Record<string, IInboxIssueStore>; // issue_id -> IInboxIssueStore
|
||||
inboxIssueIds: string[];
|
||||
intakeForms: Record<string, TInboxForm>;
|
||||
// computed
|
||||
inboxFilters: Partial<TInboxIssueFilter>; // computed project inbox filters
|
||||
inboxSorting: Partial<TInboxIssueSorting>; // computed project inbox sorting
|
||||
@@ -70,9 +68,6 @@ export interface IProjectInboxStore {
|
||||
) => Promise<void>;
|
||||
fetchInboxPaginationIssues: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
fetchInboxIssueById: (workspaceSlug: string, projectId: string, inboxIssueId: string) => Promise<TInboxIssue>;
|
||||
fetchIntakeForms: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
toggleIntakeForms: (workspaceSlug: string, projectId: string, isDisabled: boolean) => Promise<void>;
|
||||
regenerateIntakeForms: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
createInboxIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@@ -94,7 +89,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
inboxIssuePaginationInfo: TInboxIssuePaginationInfo | undefined = undefined;
|
||||
inboxIssues: Record<string, IInboxIssueStore> = {};
|
||||
inboxIssueIds: string[] = [];
|
||||
intakeForms: Record<string, TInboxForm> = {};
|
||||
// services
|
||||
inboxIssueService;
|
||||
|
||||
@@ -109,7 +103,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
inboxIssuePaginationInfo: observable,
|
||||
inboxIssues: observable,
|
||||
inboxIssueIds: observable,
|
||||
intakeForms: observable,
|
||||
// computed
|
||||
inboxFilters: computed,
|
||||
inboxSorting: computed,
|
||||
@@ -317,45 +310,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
}
|
||||
};
|
||||
|
||||
fetchIntakeForms = async (workspaceSlug: string, projectId: string) => {
|
||||
try {
|
||||
const intakeForms = await this.inboxIssueService.retrievePublishForm(workspaceSlug, projectId);
|
||||
if (intakeForms)
|
||||
runInAction(() => {
|
||||
set(this.intakeForms, projectId, intakeForms);
|
||||
});
|
||||
} catch {
|
||||
console.error("Error fetching the publish forms");
|
||||
}
|
||||
};
|
||||
|
||||
toggleIntakeForms = async (workspaceSlug: string, projectId: string, isDisabled: boolean) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.intakeForms, projectId, { ...this.intakeForms[projectId], is_disabled: isDisabled });
|
||||
});
|
||||
await this.inboxIssueService.updatePublishForm(workspaceSlug, projectId, isDisabled);
|
||||
} catch {
|
||||
console.error("Error fetching the publish forms");
|
||||
runInAction(() => {
|
||||
set(this.intakeForms, projectId, { ...this.intakeForms[projectId], is_disabled: !isDisabled });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
regenerateIntakeForms = async (workspaceSlug: string, projectId: string) => {
|
||||
try {
|
||||
const form = await this.inboxIssueService.regeneratePublishForm(workspaceSlug, projectId);
|
||||
if (form) {
|
||||
runInAction(() => {
|
||||
set(this.intakeForms, projectId, form);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
console.error("Error fetching the publish forms");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description fetch intake issues with paginated data
|
||||
* @param workspaceSlug
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "ce/components/projects/settings/intake";
|
||||
@@ -2,6 +2,7 @@
|
||||
import { TFileHandler } from "@plane/editor";
|
||||
// helpers
|
||||
import { getBase64Image, getFileURL } from "@/helpers/file.helper";
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
@@ -45,7 +46,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
if (checkURLValidity(path)) {
|
||||
return path;
|
||||
} else {
|
||||
return (
|
||||
@@ -59,7 +60,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
},
|
||||
upload: uploadFile,
|
||||
delete: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
if (checkURLValidity(src)) {
|
||||
await fileService.deleteOldWorkspaceAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.deleteNewAsset(
|
||||
@@ -72,7 +73,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
if (checkURLValidity(src)) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(workspaceSlug, src);
|
||||
@@ -96,7 +97,7 @@ export const getReadOnlyEditorFileHandlers = (
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
if (checkURLValidity(path)) {
|
||||
return path;
|
||||
} else {
|
||||
return (
|
||||
|
||||
+3
-2
@@ -66,7 +66,8 @@
|
||||
"use-debounce": "^9.0.4",
|
||||
"use-font-face-observer": "^1.2.2",
|
||||
"uuid": "^9.0.0",
|
||||
"zxcvbn": "^4.4.2"
|
||||
"zxcvbn": "^4.4.2",
|
||||
"recharts": "^2.12.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
@@ -75,7 +76,7 @@
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^8.3.4",
|
||||
|
||||
Reference in New Issue
Block a user