Compare commits

..
Author SHA1 Message Date
Aaryan Khandelwal a5fa46929b feat: toggle heading block 2024-10-21 16:50:30 +05:30
96 changed files with 1607 additions and 1481 deletions
+10 -10
View File
@@ -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
View File
@@ -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",
+1 -2
View File
@@ -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
+2 -2
View File
@@ -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(
+1 -1
View File
@@ -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",
+2 -9
View File
@@ -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())),
),
+6 -6
View File
@@ -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(
+7 -3
View File
@@ -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,
+2 -2
View File
@@ -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(
+1 -4
View File
@@ -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")
+1 -6
View File
@@ -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(
+7 -5
View File
@@ -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(
+16 -39
View File
@@ -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())),
),
+25 -57
View File
@@ -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())),
),
+7 -6
View File
@@ -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(
+41 -50
View File
@@ -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())),
),
+13 -14
View File
@@ -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),
+23 -18
View File
@@ -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())),
),
+14 -6
View File
@@ -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(
+25 -34
View File
@@ -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(
+22 -25
View File
@@ -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,
+21 -19
View File
@@ -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())),
+6 -5
View File
@@ -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")
+4 -4
View File
@@ -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}"))
@@ -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",
),
),
]
+1 -9
View File
@@ -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"
+1 -9
View File
@@ -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"
-1
View File
@@ -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)
+1
View File
@@ -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
+2
View File
@@ -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
+2 -2
View File
@@ -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(
+2 -10
View File
@@ -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),
+17 -18
View File
@@ -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())),
),
+3 -8
View File
@@ -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)
),
),
}
+8 -22
View File
@@ -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
+3
View File
@@ -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"
}
+5 -4
View File
@@ -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">;
+32 -7
View File
@@ -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);
+2 -2
View File
@@ -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": {
-13
View File
@@ -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;
};
+4 -6
View File
@@ -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",
+3 -1
View File
@@ -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",
+1 -2
View File
@@ -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(
-1
View File
@@ -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) => {
+5 -4
View File
@@ -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
View File
@@ -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";
+8 -15
View File
@@ -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 = {
+11 -11
View File
@@ -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>
);
})}
-1
View File
@@ -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 -27
View File
@@ -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";
+5 -4
View File
@@ -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
View File
@@ -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",
+788 -540
View File
File diff suppressed because it is too large Load Diff