mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' into packaging-tiptap
This commit is contained in:
commit
46c761429f
@ -33,14 +33,9 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
- space/**
|
- space/**
|
||||||
|
|
||||||
- name: Setup .npmrc for repository
|
|
||||||
run: |
|
|
||||||
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
|
|
||||||
|
|
||||||
- name: Build Plane's Main App
|
- name: Build Plane's Main App
|
||||||
if: steps.changed-files.outputs.web_any_changed == 'true'
|
if: steps.changed-files.outputs.web_any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
mv ./.npmrc ./web
|
|
||||||
cd web
|
cd web
|
||||||
yarn
|
yarn
|
||||||
yarn build
|
yarn build
|
||||||
|
4
.github/workflows/Update_Docker_Images.yml
vendored
4
.github/workflows/Update_Docker_Images.yml
vendored
@ -22,10 +22,6 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Setup .npmrc for repository
|
|
||||||
run: |
|
|
||||||
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
id: metaFrontend
|
id: metaFrontend
|
||||||
uses: docker/metadata-action@v4.3.0
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Backend
|
# Backend
|
||||||
# Debug value for api server use it as 0 for production use
|
# Debug value for api server use it as 0 for production use
|
||||||
DEBUG=0
|
DEBUG=0
|
||||||
|
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
|
||||||
|
|
||||||
# Error logs
|
# Error logs
|
||||||
SENTRY_DSN=""
|
SENTRY_DSN=""
|
||||||
|
@ -23,7 +23,7 @@ from .project import (
|
|||||||
ProjectPublicMemberSerializer
|
ProjectPublicMemberSerializer
|
||||||
)
|
)
|
||||||
from .state import StateSerializer, StateLiteSerializer
|
from .state import StateSerializer, StateLiteSerializer
|
||||||
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
|
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
|
||||||
from .asset import FileAssetSerializer
|
from .asset import FileAssetSerializer
|
||||||
from .issue import (
|
from .issue import (
|
||||||
|
@ -5,10 +5,39 @@ from rest_framework import serializers
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import IssueView, IssueViewFavorite
|
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalViewSerializer(BaseSerializer):
|
||||||
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GlobalView
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"query",
|
||||||
|
]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
query_params = validated_data.get("query_data", {})
|
||||||
|
if bool(query_params):
|
||||||
|
validated_data["query"] = issue_filters(query_params, "POST")
|
||||||
|
else:
|
||||||
|
validated_data["query"] = dict()
|
||||||
|
return GlobalView.objects.create(**validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
query_params = validated_data.get("query_data", {})
|
||||||
|
if bool(query_params):
|
||||||
|
validated_data["query"] = issue_filters(query_params, "POST")
|
||||||
|
else:
|
||||||
|
validated_data["query"] = dict()
|
||||||
|
validated_data["query"] = issue_filters(query_params, "PATCH")
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSerializer(BaseSerializer):
|
class IssueViewSerializer(BaseSerializer):
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
|
@ -102,6 +102,8 @@ from plane.api.views import (
|
|||||||
BulkEstimatePointEndpoint,
|
BulkEstimatePointEndpoint,
|
||||||
## End Estimates
|
## End Estimates
|
||||||
# Views
|
# Views
|
||||||
|
GlobalViewViewSet,
|
||||||
|
GlobalViewIssuesViewSet,
|
||||||
IssueViewViewSet,
|
IssueViewViewSet,
|
||||||
ViewIssuesEndpoint,
|
ViewIssuesEndpoint,
|
||||||
IssueViewFavoriteViewSet,
|
IssueViewFavoriteViewSet,
|
||||||
@ -184,7 +186,6 @@ from plane.api.views import (
|
|||||||
## Exporter
|
## Exporter
|
||||||
ExportIssuesEndpoint,
|
ExportIssuesEndpoint,
|
||||||
## End Exporter
|
## End Exporter
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -241,7 +242,11 @@ urlpatterns = [
|
|||||||
UpdateUserTourCompletedEndpoint.as_view(),
|
UpdateUserTourCompletedEndpoint.as_view(),
|
||||||
name="user-tour",
|
name="user-tour",
|
||||||
),
|
),
|
||||||
path("users/workspaces/<str:slug>/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
|
path(
|
||||||
|
"users/workspaces/<str:slug>/activities/",
|
||||||
|
UserActivityEndpoint.as_view(),
|
||||||
|
name="user-activities",
|
||||||
|
),
|
||||||
# user workspaces
|
# user workspaces
|
||||||
path(
|
path(
|
||||||
"users/me/workspaces/",
|
"users/me/workspaces/",
|
||||||
@ -649,6 +654,37 @@ urlpatterns = [
|
|||||||
ViewIssuesEndpoint.as_view(),
|
ViewIssuesEndpoint.as_view(),
|
||||||
name="project-view-issues",
|
name="project-view-issues",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/views/",
|
||||||
|
GlobalViewViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="global-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/views/<uuid:pk>/",
|
||||||
|
GlobalViewViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="global-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/issues/",
|
||||||
|
GlobalViewIssuesViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="global-view-issues",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
|
||||||
IssueViewFavoriteViewSet.as_view(
|
IssueViewFavoriteViewSet.as_view(
|
||||||
@ -767,11 +803,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="project-issue",
|
name="project-issue",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/issues/",
|
|
||||||
WorkSpaceIssuesEndpoint.as_view(),
|
|
||||||
name="workspace-issue",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||||
LabelViewSet.as_view(
|
LabelViewSet.as_view(
|
||||||
|
@ -56,7 +56,7 @@ from .workspace import (
|
|||||||
LeaveWorkspaceEndpoint,
|
LeaveWorkspaceEndpoint,
|
||||||
)
|
)
|
||||||
from .state import StateViewSet
|
from .state import StateViewSet
|
||||||
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||||
from .cycle import (
|
from .cycle import (
|
||||||
CycleViewSet,
|
CycleViewSet,
|
||||||
CycleIssueViewSet,
|
CycleIssueViewSet,
|
||||||
|
@ -80,6 +80,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
@ -487,6 +488,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -662,6 +664,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return all Cycle Issues
|
# Return all Cycle Issues
|
||||||
|
@ -213,6 +213,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -277,6 +278,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -518,6 +520,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -582,6 +585,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||||
|
@ -4,6 +4,7 @@ import random
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Prefetch,
|
Prefetch,
|
||||||
OuterRef,
|
OuterRef,
|
||||||
@ -129,6 +130,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().time())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -149,6 +151,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -315,6 +318,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -568,6 +572,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@ -586,6 +591,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(current_instance).data,
|
IssueCommentSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -607,6 +613,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(current_instance).data,
|
IssueCommentSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -890,6 +897,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@ -908,6 +916,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
IssueLinkSerializer(current_instance).data,
|
IssueLinkSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -929,6 +938,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
IssueLinkSerializer(current_instance).data,
|
IssueLinkSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -1007,6 +1017,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||||||
serializer.data,
|
serializer.data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1029,6 +1040,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1231,6 +1243,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
@ -1435,6 +1448,7 @@ class IssueReactionViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||||
@ -1458,6 +1472,7 @@ class IssueReactionViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_reaction.id),
|
"identifier": str(issue_reaction.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_reaction.delete()
|
issue_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1506,6 +1521,7 @@ class CommentReactionViewSet(BaseViewSet):
|
|||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||||
@ -1530,6 +1546,7 @@ class CommentReactionViewSet(BaseViewSet):
|
|||||||
"comment_id": str(comment_id),
|
"comment_id": str(comment_id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment_reaction.delete()
|
comment_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1626,6 +1643,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -1675,6 +1693,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(comment).data,
|
IssueCommentSerializer(comment).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1708,6 +1727,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(comment).data,
|
IssueCommentSerializer(comment).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment.delete()
|
comment.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1782,6 +1802,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1826,6 +1847,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_reaction.id),
|
"identifier": str(issue_reaction.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_reaction.delete()
|
issue_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1899,6 +1921,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1950,6 +1973,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
"comment_id": str(comment_id),
|
"comment_id": str(comment_id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment_reaction.delete()
|
comment_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -2013,6 +2037,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
serializer = IssueVoteSerializer(issue_vote)
|
serializer = IssueVoteSerializer(issue_vote)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@ -2047,6 +2072,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_vote.id),
|
"identifier": str(issue_vote.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_vote.delete()
|
issue_vote.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -2080,6 +2106,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
IssueRelationSerializer(current_instance).data,
|
IssueRelationSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -2113,6 +2140,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
if relation == "blocking":
|
if relation == "blocking":
|
||||||
@ -2157,6 +2185,8 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
.select_related("issue")
|
.select_related("issue")
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IssueRetrievePublicEndpoint(BaseAPIView):
|
class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
@ -2382,6 +2412,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().time())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -2566,6 +2597,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
# Django Imports
|
# Django Imports
|
||||||
|
from django.utils import timezone
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
@ -129,6 +130,7 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
@ -277,6 +279,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -444,6 +447,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -1,4 +1,18 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Case,
|
||||||
|
Value,
|
||||||
|
CharField,
|
||||||
|
When,
|
||||||
|
Exists,
|
||||||
|
Max,
|
||||||
|
)
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Prefetch, OuterRef, Exists
|
from django.db.models import Prefetch, OuterRef, Exists
|
||||||
|
|
||||||
@ -10,18 +24,192 @@ from sentry_sdk import capture_exception
|
|||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet, BaseAPIView
|
from . import BaseViewSet, BaseAPIView
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
|
GlobalViewSerializer,
|
||||||
IssueViewSerializer,
|
IssueViewSerializer,
|
||||||
IssueLiteSerializer,
|
IssueLiteSerializer,
|
||||||
IssueViewFavoriteSerializer,
|
IssueViewFavoriteSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import ProjectEntityPermission
|
from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
|
Workspace,
|
||||||
|
GlobalView,
|
||||||
IssueView,
|
IssueView,
|
||||||
Issue,
|
Issue,
|
||||||
IssueViewFavorite,
|
IssueViewFavorite,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.utils.grouper import group_results
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalViewViewSet(BaseViewSet):
|
||||||
|
serializer_class = GlobalViewSerializer
|
||||||
|
model = GlobalView
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
||||||
|
serializer.save(workspace_id=workspace.id)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace")
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalViewIssuesViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
Issue.issue_objects.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("state")
|
||||||
|
.select_related("parent")
|
||||||
|
.prefetch_related("assignees")
|
||||||
|
.prefetch_related("labels")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug):
|
||||||
|
try:
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
|
# Custom ordering for priority and state
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
|
|
||||||
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
|
issue_queryset = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(**filters)
|
||||||
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
|
priority_order = (
|
||||||
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("priority_order")
|
||||||
|
|
||||||
|
# State Ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"state__name",
|
||||||
|
"state__group",
|
||||||
|
"-state__name",
|
||||||
|
"-state__group",
|
||||||
|
]:
|
||||||
|
state_order = (
|
||||||
|
state_order
|
||||||
|
if order_by_param in ["state__name", "state__group"]
|
||||||
|
else state_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
state_order=Case(
|
||||||
|
*[
|
||||||
|
When(state__group=state_group, then=Value(i))
|
||||||
|
for i, state_group in enumerate(state_order)
|
||||||
|
],
|
||||||
|
default=Value(len(state_order)),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("state_order")
|
||||||
|
# assignee and label ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"labels__name",
|
||||||
|
"-labels__name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"-assignees__first_name",
|
||||||
|
]:
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
max_values=Max(
|
||||||
|
order_by_param[1::]
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else order_by_param
|
||||||
|
)
|
||||||
|
).order_by(
|
||||||
|
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
|
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||||
|
|
||||||
|
## Grouping the results
|
||||||
|
group_by = request.GET.get("group_by", False)
|
||||||
|
sub_group_by = request.GET.get("sub_group_by", False)
|
||||||
|
if sub_group_by and sub_group_by == group_by:
|
||||||
|
return Response(
|
||||||
|
{"error": "Group by and sub group by cannot be same"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if group_by:
|
||||||
|
return Response(
|
||||||
|
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewViewSet(BaseViewSet):
|
class IssueViewViewSet(BaseViewSet):
|
||||||
|
@ -39,6 +39,7 @@ def track_name(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("name") != requested_data.get("name"):
|
if current_instance.get("name") != requested_data.get("name"):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
@ -52,6 +53,7 @@ def track_name(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the name to {requested_data.get('name')}",
|
comment=f"updated the name to {requested_data.get('name')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -64,6 +66,7 @@ def track_parent(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("parent") != requested_data.get("parent"):
|
if current_instance.get("parent") != requested_data.get("parent"):
|
||||||
if requested_data.get("parent") == None:
|
if requested_data.get("parent") == None:
|
||||||
@ -81,6 +84,7 @@ def track_parent(
|
|||||||
comment=f"updated the parent issue to None",
|
comment=f"updated the parent issue to None",
|
||||||
old_identifier=old_parent.id,
|
old_identifier=old_parent.id,
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -101,6 +105,7 @@ def track_parent(
|
|||||||
comment=f"updated the parent issue to {new_parent.name}",
|
comment=f"updated the parent issue to {new_parent.name}",
|
||||||
old_identifier=old_parent.id if old_parent is not None else None,
|
old_identifier=old_parent.id if old_parent is not None else None,
|
||||||
new_identifier=new_parent.id,
|
new_identifier=new_parent.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -113,6 +118,7 @@ def track_priority(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("priority") != requested_data.get("priority"):
|
if current_instance.get("priority") != requested_data.get("priority"):
|
||||||
if requested_data.get("priority") == None:
|
if requested_data.get("priority") == None:
|
||||||
@ -127,6 +133,7 @@ def track_priority(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the priority to None",
|
comment=f"updated the priority to None",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -141,6 +148,7 @@ def track_priority(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the priority to {requested_data.get('priority')}",
|
comment=f"updated the priority to {requested_data.get('priority')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -153,6 +161,7 @@ def track_state(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("state") != requested_data.get("state"):
|
if current_instance.get("state") != requested_data.get("state"):
|
||||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
new_state = State.objects.get(pk=requested_data.get("state", None))
|
||||||
@ -171,6 +180,7 @@ def track_state(
|
|||||||
comment=f"updated the state to {new_state.name}",
|
comment=f"updated the state to {new_state.name}",
|
||||||
old_identifier=old_state.id,
|
old_identifier=old_state.id,
|
||||||
new_identifier=new_state.id,
|
new_identifier=new_state.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -183,6 +193,7 @@ def track_description(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("description_html") != requested_data.get(
|
if current_instance.get("description_html") != requested_data.get(
|
||||||
"description_html"
|
"description_html"
|
||||||
@ -203,6 +214,7 @@ def track_description(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the description to {requested_data.get('description_html')}",
|
comment=f"updated the description to {requested_data.get('description_html')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -215,6 +227,7 @@ def track_target_date(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("target_date") != requested_data.get("target_date"):
|
if current_instance.get("target_date") != requested_data.get("target_date"):
|
||||||
if requested_data.get("target_date") == None:
|
if requested_data.get("target_date") == None:
|
||||||
@ -229,6 +242,7 @@ def track_target_date(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the target date to None",
|
comment=f"updated the target date to None",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -243,6 +257,7 @@ def track_target_date(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the target date to {requested_data.get('target_date')}",
|
comment=f"updated the target date to {requested_data.get('target_date')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -255,6 +270,7 @@ def track_start_date(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("start_date") != requested_data.get("start_date"):
|
if current_instance.get("start_date") != requested_data.get("start_date"):
|
||||||
if requested_data.get("start_date") == None:
|
if requested_data.get("start_date") == None:
|
||||||
@ -269,6 +285,7 @@ def track_start_date(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the start date to None",
|
comment=f"updated the start date to None",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -283,6 +300,7 @@ def track_start_date(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the start date to {requested_data.get('start_date')}",
|
comment=f"updated the start date to {requested_data.get('start_date')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -295,6 +313,7 @@ def track_labels(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
# Label Addition
|
# Label Addition
|
||||||
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
||||||
@ -314,6 +333,7 @@ def track_labels(
|
|||||||
comment=f"added label {label.name}",
|
comment=f"added label {label.name}",
|
||||||
new_identifier=label.id,
|
new_identifier=label.id,
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -335,6 +355,7 @@ def track_labels(
|
|||||||
comment=f"removed label {label.name}",
|
comment=f"removed label {label.name}",
|
||||||
old_identifier=label.id,
|
old_identifier=label.id,
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -347,6 +368,7 @@ def track_assignees(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
# Assignee Addition
|
# Assignee Addition
|
||||||
if len(requested_data.get("assignees_list")) > len(
|
if len(requested_data.get("assignees_list")) > len(
|
||||||
@ -367,6 +389,7 @@ def track_assignees(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"added assignee {assignee.display_name}",
|
comment=f"added assignee {assignee.display_name}",
|
||||||
new_identifier=assignee.id,
|
new_identifier=assignee.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -389,12 +412,13 @@ def track_assignees(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"removed assignee {assignee.display_name}",
|
comment=f"removed assignee {assignee.display_name}",
|
||||||
old_identifier=assignee.id,
|
old_identifier=assignee.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_issue_activity(
|
def create_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -404,12 +428,13 @@ def create_issue_activity(
|
|||||||
comment=f"created the issue",
|
comment=f"created the issue",
|
||||||
verb="created",
|
verb="created",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def track_estimate_points(
|
def track_estimate_points(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
|
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
|
||||||
if requested_data.get("estimate_point") == None:
|
if requested_data.get("estimate_point") == None:
|
||||||
@ -424,6 +449,7 @@ def track_estimate_points(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the estimate point to None",
|
comment=f"updated the estimate point to None",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -438,12 +464,13 @@ def track_estimate_points(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
|
comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def track_archive_at(
|
def track_archive_at(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
if requested_data.get("archived_at") is None:
|
if requested_data.get("archived_at") is None:
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
@ -457,6 +484,7 @@ def track_archive_at(
|
|||||||
field="archived_at",
|
field="archived_at",
|
||||||
old_value="archive",
|
old_value="archive",
|
||||||
new_value="restore",
|
new_value="restore",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -471,12 +499,13 @@ def track_archive_at(
|
|||||||
field="archived_at",
|
field="archived_at",
|
||||||
old_value=None,
|
old_value=None,
|
||||||
new_value="archive",
|
new_value="archive",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def track_closed_to(
|
def track_closed_to(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
if requested_data.get("closed_to") is not None:
|
if requested_data.get("closed_to") is not None:
|
||||||
updated_state = State.objects.get(
|
updated_state = State.objects.get(
|
||||||
@ -496,12 +525,13 @@ def track_closed_to(
|
|||||||
comment=f"Plane updated the state to {updated_state.name}",
|
comment=f"Plane updated the state to {updated_state.name}",
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
new_identifier=updated_state.id,
|
new_identifier=updated_state.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_issue_activity(
|
def update_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
ISSUE_ACTIVITY_MAPPER = {
|
ISSUE_ACTIVITY_MAPPER = {
|
||||||
"name": track_name,
|
"name": track_name,
|
||||||
@ -518,6 +548,11 @@ def update_issue_activity(
|
|||||||
"closed_to": track_closed_to,
|
"closed_to": track_closed_to,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
for key in requested_data:
|
for key in requested_data:
|
||||||
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
||||||
if func is not None:
|
if func is not None:
|
||||||
@ -528,11 +563,12 @@ def update_issue_activity(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_issue_activity(
|
def delete_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -542,12 +578,13 @@ def delete_issue_activity(
|
|||||||
verb="deleted",
|
verb="deleted",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
field="issue",
|
field="issue",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_comment_activity(
|
def create_comment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -566,12 +603,13 @@ def create_comment_activity(
|
|||||||
new_value=requested_data.get("comment_html", ""),
|
new_value=requested_data.get("comment_html", ""),
|
||||||
new_identifier=requested_data.get("id", None),
|
new_identifier=requested_data.get("id", None),
|
||||||
issue_comment_id=requested_data.get("id", None),
|
issue_comment_id=requested_data.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_comment_activity(
|
def update_comment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -593,12 +631,13 @@ def update_comment_activity(
|
|||||||
new_value=requested_data.get("comment_html", ""),
|
new_value=requested_data.get("comment_html", ""),
|
||||||
new_identifier=current_instance.get("id", None),
|
new_identifier=current_instance.get("id", None),
|
||||||
issue_comment_id=current_instance.get("id", None),
|
issue_comment_id=current_instance.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_comment_activity(
|
def delete_comment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -609,12 +648,13 @@ def delete_comment_activity(
|
|||||||
verb="deleted",
|
verb="deleted",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
field="comment",
|
field="comment",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_cycle_issue_activity(
|
def create_cycle_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -646,6 +686,7 @@ def create_cycle_issue_activity(
|
|||||||
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
|
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||||
old_identifier=old_cycle.id,
|
old_identifier=old_cycle.id,
|
||||||
new_identifier=new_cycle.id,
|
new_identifier=new_cycle.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -666,12 +707,13 @@ def create_cycle_issue_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"added cycle {cycle.name}",
|
comment=f"added cycle {cycle.name}",
|
||||||
new_identifier=cycle.id,
|
new_identifier=cycle.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_cycle_issue_activity(
|
def delete_cycle_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -695,12 +737,13 @@ def delete_cycle_issue_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"removed this issue from {cycle.name if cycle is not None else None}",
|
comment=f"removed this issue from {cycle.name if cycle is not None else None}",
|
||||||
old_identifier=cycle.id if cycle is not None else None,
|
old_identifier=cycle.id if cycle is not None else None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_module_issue_activity(
|
def create_module_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -732,6 +775,7 @@ def create_module_issue_activity(
|
|||||||
comment=f"updated module from {old_module.name} to {new_module.name}",
|
comment=f"updated module from {old_module.name} to {new_module.name}",
|
||||||
old_identifier=old_module.id,
|
old_identifier=old_module.id,
|
||||||
new_identifier=new_module.id,
|
new_identifier=new_module.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -751,12 +795,13 @@ def create_module_issue_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"added module {module.name}",
|
comment=f"added module {module.name}",
|
||||||
new_identifier=module.id,
|
new_identifier=module.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_module_issue_activity(
|
def delete_module_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -780,12 +825,13 @@ def delete_module_issue_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"removed this issue from {module.name if module is not None else None}",
|
comment=f"removed this issue from {module.name if module is not None else None}",
|
||||||
old_identifier=module.id if module is not None else None,
|
old_identifier=module.id if module is not None else None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_link_activity(
|
def create_link_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -803,12 +849,13 @@ def create_link_activity(
|
|||||||
field="link",
|
field="link",
|
||||||
new_value=requested_data.get("url", ""),
|
new_value=requested_data.get("url", ""),
|
||||||
new_identifier=requested_data.get("id", None),
|
new_identifier=requested_data.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_link_activity(
|
def update_link_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -829,12 +876,13 @@ def update_link_activity(
|
|||||||
old_identifier=current_instance.get("id"),
|
old_identifier=current_instance.get("id"),
|
||||||
new_value=requested_data.get("url", ""),
|
new_value=requested_data.get("url", ""),
|
||||||
new_identifier=current_instance.get("id", None),
|
new_identifier=current_instance.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_link_activity(
|
def delete_link_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
|
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -851,13 +899,14 @@ def delete_link_activity(
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
field="link",
|
field="link",
|
||||||
old_value=current_instance.get("url", ""),
|
old_value=current_instance.get("url", ""),
|
||||||
new_value=""
|
new_value="",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_attachment_activity(
|
def create_attachment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -875,12 +924,13 @@ def create_attachment_activity(
|
|||||||
field="attachment",
|
field="attachment",
|
||||||
new_value=current_instance.get("asset", ""),
|
new_value=current_instance.get("asset", ""),
|
||||||
new_identifier=current_instance.get("id", None),
|
new_identifier=current_instance.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_attachment_activity(
|
def delete_attachment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -891,11 +941,12 @@ def delete_attachment_activity(
|
|||||||
verb="deleted",
|
verb="deleted",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
field="attachment",
|
field="attachment",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_issue_reaction_activity(
|
def create_issue_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
if requested_data and requested_data.get("reaction") is not None:
|
if requested_data and requested_data.get("reaction") is not None:
|
||||||
@ -914,12 +965,13 @@ def create_issue_reaction_activity(
|
|||||||
comment="added the reaction",
|
comment="added the reaction",
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
new_identifier=issue_reaction,
|
new_identifier=issue_reaction,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_issue_reaction_activity(
|
def delete_issue_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
@ -938,12 +990,13 @@ def delete_issue_reaction_activity(
|
|||||||
comment="removed the reaction",
|
comment="removed the reaction",
|
||||||
old_identifier=current_instance.get("identifier"),
|
old_identifier=current_instance.get("identifier"),
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_comment_reaction_activity(
|
def create_comment_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
if requested_data and requested_data.get("reaction") is not None:
|
if requested_data and requested_data.get("reaction") is not None:
|
||||||
@ -963,12 +1016,13 @@ def create_comment_reaction_activity(
|
|||||||
comment="added the reaction",
|
comment="added the reaction",
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
new_identifier=comment_reaction_id,
|
new_identifier=comment_reaction_id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_comment_reaction_activity(
|
def delete_comment_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
@ -989,12 +1043,13 @@ def delete_comment_reaction_activity(
|
|||||||
comment="removed the reaction",
|
comment="removed the reaction",
|
||||||
old_identifier=current_instance.get("identifier"),
|
old_identifier=current_instance.get("identifier"),
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_issue_vote_activity(
|
def create_issue_vote_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
if requested_data and requested_data.get("vote") is not None:
|
if requested_data and requested_data.get("vote") is not None:
|
||||||
@ -1011,12 +1066,13 @@ def create_issue_vote_activity(
|
|||||||
comment="added the vote",
|
comment="added the vote",
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_issue_vote_activity(
|
def delete_issue_vote_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
@ -1035,12 +1091,13 @@ def delete_issue_vote_activity(
|
|||||||
comment="removed the vote",
|
comment="removed the vote",
|
||||||
old_identifier=current_instance.get("identifier"),
|
old_identifier=current_instance.get("identifier"),
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_issue_relation_activity(
|
def create_issue_relation_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -1080,12 +1137,13 @@ def create_issue_relation_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f'added {issue_relation.get("relation_type")} relation',
|
comment=f'added {issue_relation.get("relation_type")} relation',
|
||||||
old_identifier=issue_relation.get("related_issue"),
|
old_identifier=issue_relation.get("related_issue"),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_issue_relation_activity(
|
def delete_issue_relation_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -1109,6 +1167,7 @@ def delete_issue_relation_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f'deleted {relation_type} relation',
|
comment=f'deleted {relation_type} relation',
|
||||||
old_identifier=current_instance.get("issue"),
|
old_identifier=current_instance.get("issue"),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
|
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
|
||||||
@ -1124,12 +1183,13 @@ def delete_issue_relation_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f'deleted {current_instance.get("relation_type")} relation',
|
comment=f'deleted {current_instance.get("relation_type")} relation',
|
||||||
old_identifier=current_instance.get("related_issue"),
|
old_identifier=current_instance.get("related_issue"),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_draft_issue_activity(
|
def create_draft_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -1140,12 +1200,13 @@ def create_draft_issue_activity(
|
|||||||
field="draft",
|
field="draft",
|
||||||
verb="created",
|
verb="created",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_draft_issue_activity(
|
def update_draft_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -1160,6 +1221,7 @@ def update_draft_issue_activity(
|
|||||||
comment=f"created the issue",
|
comment=f"created the issue",
|
||||||
verb="updated",
|
verb="updated",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -1172,13 +1234,14 @@ def update_draft_issue_activity(
|
|||||||
field="draft",
|
field="draft",
|
||||||
verb="updated",
|
verb="updated",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def delete_draft_issue_activity(
|
def delete_draft_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -1188,6 +1251,7 @@ def delete_draft_issue_activity(
|
|||||||
field="draft",
|
field="draft",
|
||||||
verb="deleted",
|
verb="deleted",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1200,6 +1264,7 @@ def issue_activity(
|
|||||||
issue_id,
|
issue_id,
|
||||||
actor_id,
|
actor_id,
|
||||||
project_id,
|
project_id,
|
||||||
|
epoch,
|
||||||
subscriber=True,
|
subscriber=True,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
@ -1276,6 +1341,7 @@ def issue_activity(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save all the values to database
|
# Save all the values to database
|
||||||
|
@ -77,6 +77,7 @@ def archive_old_issues():
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
subscriber=False,
|
subscriber=False,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
for issue in updated_issues
|
||||||
]
|
]
|
||||||
@ -148,6 +149,7 @@ def close_old_issues():
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
subscriber=False,
|
subscriber=False,
|
||||||
|
epoch = int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
for issue in updated_issues
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
||||||
|
|
||||||
from django.db import migrations
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
def update_issue_activity(apps, schema_editor):
|
def update_issue_activity(apps, schema_editor):
|
||||||
IssueActivityModel = apps.get_model("db", "IssueActivity")
|
IssueActivityModel = apps.get_model("db", "IssueActivity")
|
||||||
@ -19,5 +23,27 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GlobalView',
|
||||||
|
fields=[
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||||
|
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='View Name')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='View Description')),
|
||||||
|
('query', models.JSONField(verbose_name='View Query')),
|
||||||
|
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
|
||||||
|
('query_data', models.JSONField(default=dict)),
|
||||||
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||||
|
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Global View',
|
||||||
|
'verbose_name_plural': 'Global Views',
|
||||||
|
'db_table': 'global_views',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.RunPython(update_issue_activity),
|
migrations.RunPython(update_issue_activity),
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-09-14 06:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
def update_epoch(apps, schema_editor):
|
||||||
|
IssueActivity = apps.get_model('db', 'IssueActivity')
|
||||||
|
updated_issue_activity = []
|
||||||
|
for obj in IssueActivity.objects.all():
|
||||||
|
obj.epoch = int(obj.created_at.timestamp())
|
||||||
|
updated_issue_activity.append(obj)
|
||||||
|
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0044_auto_20230913_0709'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='epoch',
|
||||||
|
field=models.FloatField(null=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(update_epoch),
|
||||||
|
]
|
@ -50,7 +50,7 @@ from .state import State
|
|||||||
|
|
||||||
from .cycle import Cycle, CycleIssue, CycleFavorite
|
from .cycle import Cycle, CycleIssue, CycleFavorite
|
||||||
|
|
||||||
from .view import IssueView, IssueViewFavorite
|
from .view import GlobalView, IssueView, IssueViewFavorite
|
||||||
|
|
||||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
|
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
|
||||||
|
|
||||||
|
@ -309,6 +309,7 @@ class IssueActivity(ProjectBaseModel):
|
|||||||
)
|
)
|
||||||
old_identifier = models.UUIDField(null=True)
|
old_identifier = models.UUIDField(null=True)
|
||||||
new_identifier = models.UUIDField(null=True)
|
new_identifier = models.UUIDField(null=True)
|
||||||
|
epoch = models.FloatField(null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Issue Activity"
|
verbose_name = "Issue Activity"
|
||||||
|
@ -3,7 +3,30 @@ from django.db import models
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
# Module import
|
# Module import
|
||||||
from . import ProjectBaseModel
|
from . import ProjectBaseModel, BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalView(BaseModel):
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=255, verbose_name="View Name")
|
||||||
|
description = models.TextField(verbose_name="View Description", blank=True)
|
||||||
|
query = models.JSONField(verbose_name="View Query")
|
||||||
|
access = models.PositiveSmallIntegerField(
|
||||||
|
default=1, choices=((0, "Private"), (1, "Public"))
|
||||||
|
)
|
||||||
|
query_data = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Global View"
|
||||||
|
verbose_name_plural = "Global Views"
|
||||||
|
db_table = "global_views"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return name of the View"""
|
||||||
|
return f"{self.name} <{self.workspace.name}>"
|
||||||
|
|
||||||
|
|
||||||
class IssueView(ProjectBaseModel):
|
class IssueView(ProjectBaseModel):
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
"""Production settings and globals."""
|
"""Production settings and globals."""
|
||||||
from urllib.parse import urlparse
|
|
||||||
import ssl
|
import ssl
|
||||||
import certifi
|
import certifi
|
||||||
|
|
||||||
import dj_database_url
|
import dj_database_url
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
@ -91,112 +89,89 @@ if bool(os.environ.get("SENTRY_DSN", False)):
|
|||||||
profiles_sample_rate=1.0,
|
profiles_sample_rate=1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
if DOCKERIZED and USE_MINIO:
|
# The AWS region to connect to.
|
||||||
INSTALLED_APPS += ("storages",)
|
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||||
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
|
|
||||||
# The AWS access key to use.
|
|
||||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
|
||||||
# The AWS secret access key to use.
|
|
||||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
|
|
||||||
# The name of the bucket to store files in.
|
|
||||||
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
|
||||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
|
||||||
AWS_S3_ENDPOINT_URL = os.environ.get(
|
|
||||||
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
|
|
||||||
)
|
|
||||||
# Default permissions
|
|
||||||
AWS_DEFAULT_ACL = "public-read"
|
|
||||||
AWS_QUERYSTRING_AUTH = False
|
|
||||||
AWS_S3_FILE_OVERWRITE = False
|
|
||||||
|
|
||||||
# Custom Domain settings
|
# The AWS access key to use.
|
||||||
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
|
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
||||||
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
|
|
||||||
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
|
|
||||||
else:
|
|
||||||
# The AWS region to connect to.
|
|
||||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
|
||||||
|
|
||||||
# The AWS access key to use.
|
# The AWS secret access key to use.
|
||||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
||||||
|
|
||||||
# The AWS secret access key to use.
|
# The optional AWS session token to use.
|
||||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
# AWS_SESSION_TOKEN = ""
|
||||||
|
|
||||||
# The optional AWS session token to use.
|
# The name of the bucket to store files in.
|
||||||
# AWS_SESSION_TOKEN = ""
|
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||||
|
|
||||||
# The name of the bucket to store files in.
|
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||||
|
|
||||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
||||||
|
|
||||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
AWS_S3_KEY_PREFIX = ""
|
||||||
|
|
||||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||||
AWS_S3_KEY_PREFIX = ""
|
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
||||||
|
# and their permissions will be set to "public-read".
|
||||||
|
AWS_S3_BUCKET_AUTH = False
|
||||||
|
|
||||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||||
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
# is True. It also affects the "Cache-Control" header of the files.
|
||||||
# and their permissions will be set to "public-read".
|
# Important: Changing this setting will not affect existing files.
|
||||||
AWS_S3_BUCKET_AUTH = False
|
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||||
|
|
||||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||||
# is True. It also affects the "Cache-Control" header of the files.
|
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||||
# Important: Changing this setting will not affect existing files.
|
AWS_S3_PUBLIC_URL = ""
|
||||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
|
||||||
|
|
||||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
# understand the consequences before enabling.
|
||||||
AWS_S3_PUBLIC_URL = ""
|
# Important: Changing this setting will not affect existing files.
|
||||||
|
AWS_S3_REDUCED_REDUNDANCY = False
|
||||||
|
|
||||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||||
# understand the consequences before enabling.
|
# single `name` argument.
|
||||||
# Important: Changing this setting will not affect existing files.
|
# Important: Changing this setting will not affect existing files.
|
||||||
AWS_S3_REDUCED_REDUNDANCY = False
|
AWS_S3_CONTENT_DISPOSITION = ""
|
||||||
|
|
||||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||||
# single `name` argument.
|
# single `name` argument.
|
||||||
# Important: Changing this setting will not affect existing files.
|
# Important: Changing this setting will not affect existing files.
|
||||||
AWS_S3_CONTENT_DISPOSITION = ""
|
AWS_S3_CONTENT_LANGUAGE = ""
|
||||||
|
|
||||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||||
# single `name` argument.
|
# single `name` argument.
|
||||||
# Important: Changing this setting will not affect existing files.
|
# Important: Changing this setting will not affect existing files.
|
||||||
AWS_S3_CONTENT_LANGUAGE = ""
|
AWS_S3_METADATA = {}
|
||||||
|
|
||||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
# If True, then files will be stored using AES256 server-side encryption.
|
||||||
# single `name` argument.
|
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||||
# Important: Changing this setting will not affect existing files.
|
# Otherwise, server-side encryption is not be enabled.
|
||||||
AWS_S3_METADATA = {}
|
# Important: Changing this setting will not affect existing files.
|
||||||
|
AWS_S3_ENCRYPT_KEY = False
|
||||||
|
|
||||||
# If True, then files will be stored using AES256 server-side encryption.
|
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||||
# Otherwise, server-side encryption is not be enabled.
|
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||||
# Important: Changing this setting will not affect existing files.
|
|
||||||
AWS_S3_ENCRYPT_KEY = False
|
|
||||||
|
|
||||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
# compressed size is smaller than their uncompressed size.
|
||||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
# Important: Changing this setting will not affect existing files.
|
||||||
|
AWS_S3_GZIP = True
|
||||||
|
|
||||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
# The signature version to use for S3 requests.
|
||||||
# compressed size is smaller than their uncompressed size.
|
AWS_S3_SIGNATURE_VERSION = None
|
||||||
# Important: Changing this setting will not affect existing files.
|
|
||||||
AWS_S3_GZIP = True
|
|
||||||
|
|
||||||
# The signature version to use for S3 requests.
|
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||||
AWS_S3_SIGNATURE_VERSION = None
|
# extra characters appended.
|
||||||
|
AWS_S3_FILE_OVERWRITE = False
|
||||||
|
|
||||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
STORAGES["default"] = {
|
||||||
# extra characters appended.
|
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||||
AWS_S3_FILE_OVERWRITE = False
|
}
|
||||||
|
|
||||||
STORAGES["default"] = {
|
|
||||||
"BACKEND": "django_s3_storage.storage.S3Storage",
|
|
||||||
}
|
|
||||||
|
|
||||||
# AWS Settings End
|
# AWS Settings End
|
||||||
|
|
||||||
@ -218,27 +193,16 @@ CSRF_COOKIE_SECURE = True
|
|||||||
|
|
||||||
REDIS_URL = os.environ.get("REDIS_URL")
|
REDIS_URL = os.environ.get("REDIS_URL")
|
||||||
|
|
||||||
if DOCKERIZED:
|
CACHES = {
|
||||||
CACHES = {
|
"default": {
|
||||||
"default": {
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
"LOCATION": REDIS_URL,
|
||||||
"LOCATION": REDIS_URL,
|
"OPTIONS": {
|
||||||
"OPTIONS": {
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
CACHES = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
|
||||||
"LOCATION": REDIS_URL,
|
|
||||||
"OPTIONS": {
|
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
||||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
|
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
|
||||||
@ -261,19 +225,16 @@ broker_url = (
|
|||||||
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if DOCKERIZED:
|
CELERY_RESULT_BACKEND = broker_url
|
||||||
CELERY_BROKER_URL = REDIS_URL
|
CELERY_BROKER_URL = broker_url
|
||||||
CELERY_RESULT_BACKEND = REDIS_URL
|
|
||||||
else:
|
|
||||||
CELERY_RESULT_BACKEND = broker_url
|
|
||||||
CELERY_BROKER_URL = broker_url
|
|
||||||
|
|
||||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||||
|
|
||||||
|
# Enable or Disable signups
|
||||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||||
|
|
||||||
# Scout Settings
|
# Scout Settings
|
||||||
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
||||||
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
||||||
SCOUT_NAME = "Plane"
|
SCOUT_NAME = "Plane"
|
||||||
|
|
||||||
|
128
apiserver/plane/settings/selfhosted.py
Normal file
128
apiserver/plane/settings/selfhosted.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
"""Self hosted settings and globals."""
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import dj_database_url
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
from .common import * # noqa
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
|
||||||
|
|
||||||
|
# Docker configurations
|
||||||
|
DOCKERIZED = 1
|
||||||
|
USE_MINIO = 1
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"NAME": "plane",
|
||||||
|
"USER": os.environ.get("PGUSER", ""),
|
||||||
|
"PASSWORD": os.environ.get("PGPASSWORD", ""),
|
||||||
|
"HOST": os.environ.get("PGHOST", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse database configuration from $DATABASE_URL
|
||||||
|
DATABASES["default"] = dj_database_url.config()
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
# File size limit
|
||||||
|
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||||
|
|
||||||
|
CORS_ALLOW_METHODS = [
|
||||||
|
"DELETE",
|
||||||
|
"GET",
|
||||||
|
"OPTIONS",
|
||||||
|
"PATCH",
|
||||||
|
"POST",
|
||||||
|
"PUT",
|
||||||
|
]
|
||||||
|
|
||||||
|
CORS_ALLOW_HEADERS = [
|
||||||
|
"accept",
|
||||||
|
"accept-encoding",
|
||||||
|
"authorization",
|
||||||
|
"content-type",
|
||||||
|
"dnt",
|
||||||
|
"origin",
|
||||||
|
"user-agent",
|
||||||
|
"x-csrftoken",
|
||||||
|
"x-requested-with",
|
||||||
|
]
|
||||||
|
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTALLED_APPS += ("storages",)
|
||||||
|
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
|
||||||
|
# The AWS access key to use.
|
||||||
|
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
||||||
|
# The AWS secret access key to use.
|
||||||
|
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
|
||||||
|
# The name of the bucket to store files in.
|
||||||
|
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
||||||
|
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||||
|
AWS_S3_ENDPOINT_URL = os.environ.get(
|
||||||
|
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
|
||||||
|
)
|
||||||
|
# Default permissions
|
||||||
|
AWS_DEFAULT_ACL = "public-read"
|
||||||
|
AWS_QUERYSTRING_AUTH = False
|
||||||
|
AWS_S3_FILE_OVERWRITE = False
|
||||||
|
|
||||||
|
# Custom Domain settings
|
||||||
|
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
|
||||||
|
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
|
||||||
|
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
|
||||||
|
|
||||||
|
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||||
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
|
||||||
|
# Allow all host headers
|
||||||
|
ALLOWED_HOSTS = [
|
||||||
|
"*",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|
||||||
|
# Redis URL
|
||||||
|
REDIS_URL = os.environ.get("REDIS_URL")
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
"LOCATION": REDIS_URL,
|
||||||
|
"OPTIONS": {
|
||||||
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# URL used for email redirects
|
||||||
|
WEB_URL = os.environ.get("WEB_URL", "http://localhost")
|
||||||
|
|
||||||
|
# Celery settings
|
||||||
|
CELERY_BROKER_URL = REDIS_URL
|
||||||
|
CELERY_RESULT_BACKEND = REDIS_URL
|
||||||
|
|
||||||
|
# Enable or Disable signups
|
||||||
|
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||||
|
|
||||||
|
# Analytics
|
||||||
|
ANALYTICS_BASE_API = False
|
||||||
|
|
||||||
|
# OPEN AI Settings
|
||||||
|
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||||
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
||||||
|
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
|
@ -4,7 +4,7 @@ x-api-and-worker-env:
|
|||||||
&api-and-worker-env
|
&api-and-worker-env
|
||||||
DEBUG: ${DEBUG}
|
DEBUG: ${DEBUG}
|
||||||
SENTRY_DSN: ${SENTRY_DSN}
|
SENTRY_DSN: ${SENTRY_DSN}
|
||||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
DJANGO_SETTINGS_MODULE: plane.settings.selfhosted
|
||||||
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
|
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
|
||||||
REDIS_URL: redis://plane-redis:6379/
|
REDIS_URL: redis://plane-redis:6379/
|
||||||
EMAIL_HOST: ${EMAIL_HOST}
|
EMAIL_HOST: ${EMAIL_HOST}
|
||||||
|
13
setup.sh
13
setup.sh
@ -10,15 +10,4 @@ cp ./space/.env.example ./space/.env
|
|||||||
cp ./apiserver/.env.example ./apiserver/.env
|
cp ./apiserver/.env.example ./apiserver/.env
|
||||||
|
|
||||||
# Generate the SECRET_KEY that will be used by django
|
# Generate the SECRET_KEY that will be used by django
|
||||||
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||||
|
|
||||||
# Generate Prompt for taking tiptap auth key
|
|
||||||
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
|
|
||||||
|
|
||||||
echo -e "\e[1;38m 1. Head over to TipTap cloud's Pro Extensions Page, https://collab.tiptap.dev/pro-extensions \e[0m"
|
|
||||||
echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, after 'Here it is' \e[0m \n"
|
|
||||||
|
|
||||||
read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken
|
|
||||||
|
|
||||||
echo "@tiptap-pro:registry=https://registry.tiptap.dev/
|
|
||||||
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
|
|
@ -41,7 +41,7 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
|
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId, inboxId } = router.query;
|
const { workspaceSlug, projectId, issueId, inboxId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
@ -183,6 +183,13 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
isOpen={isIssueModalOpen}
|
isOpen={isIssueModalOpen}
|
||||||
handleClose={() => setIsIssueModalOpen(false)}
|
handleClose={() => setIsIssueModalOpen(false)}
|
||||||
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
|
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
|
||||||
|
prePopulateData={
|
||||||
|
cycleId
|
||||||
|
? { cycle: cycleId.toString() }
|
||||||
|
: moduleId
|
||||||
|
? { module: moduleId.toString() }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<BulkDeleteIssuesModal
|
<BulkDeleteIssuesModal
|
||||||
isOpen={isBulkDeleteIssuesModalOpen}
|
isOpen={isBulkDeleteIssuesModalOpen}
|
||||||
|
@ -2,8 +2,9 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { Icon, Tooltip } from "components/ui";
|
import { Icon, Tooltip } from "components/ui";
|
||||||
|
import { CopyPlus } from "lucide-react";
|
||||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
@ -157,7 +158,7 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||||
},
|
},
|
||||||
blocks: {
|
blocked_by: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
if (activity.old_value === "")
|
if (activity.old_value === "")
|
||||||
return (
|
return (
|
||||||
@ -176,6 +177,44 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||||
},
|
},
|
||||||
|
duplicate: {
|
||||||
|
message: (activity) => {
|
||||||
|
if (activity.old_value === "")
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
marked this issue as duplicate of{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
removed this issue as a duplicate of{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: <CopyPlus size={12} color="#6b7280" />,
|
||||||
|
},
|
||||||
|
relates_to: {
|
||||||
|
message: (activity) => {
|
||||||
|
if (activity.old_value === "")
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
marked that this issue relates to{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
removed the relation from{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
|
||||||
|
},
|
||||||
cycles: {
|
cycles: {
|
||||||
message: (activity, showIssue, workspaceSlug) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
if (activity.verb === "created")
|
if (activity.verb === "created")
|
||||||
|
@ -23,7 +23,6 @@ import {
|
|||||||
CreateUpdateIssueModal,
|
CreateUpdateIssueModal,
|
||||||
DeleteIssueModal,
|
DeleteIssueModal,
|
||||||
DeleteDraftIssueModal,
|
DeleteDraftIssueModal,
|
||||||
IssuePeekOverview,
|
|
||||||
CreateUpdateDraftIssueModal,
|
CreateUpdateDraftIssueModal,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
@ -484,15 +483,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
fieldsToShow={[
|
fieldsToShow={["all"]}
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"label",
|
|
||||||
"assignee",
|
|
||||||
"priority",
|
|
||||||
"dueDate",
|
|
||||||
"priority",
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
||||||
|
@ -50,12 +50,12 @@ export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeletion = async () => {
|
||||||
if (!workspaceSlug || !data) return;
|
if (!workspaceSlug || !data || !user) return;
|
||||||
|
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await issueServices
|
await issueServices
|
||||||
.deleteDraftIssue(workspaceSlug as string, data.project, data.id)
|
.deleteDraftIssue(workspaceSlug as string, data.project, data.id, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
handleClose();
|
handleClose();
|
||||||
|
@ -14,6 +14,7 @@ import useIssuesView from "hooks/use-issues-view";
|
|||||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
import useProjects from "hooks/use-projects";
|
import useProjects from "hooks/use-projects";
|
||||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||||
// components
|
// components
|
||||||
@ -79,6 +80,8 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { projects } = useProjects();
|
const { projects } = useProjects();
|
||||||
|
|
||||||
|
const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {});
|
||||||
|
|
||||||
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -111,11 +114,14 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prePopulateData && prePopulateData.project)
|
||||||
|
return setActiveProject(prePopulateData.project);
|
||||||
|
|
||||||
// if data is not present, set active project to the project
|
// if data is not present, set active project to the project
|
||||||
// in the url. This has the least priority.
|
// in the url. This has the least priority.
|
||||||
if (projects && projects.length > 0 && !activeProject)
|
if (projects && projects.length > 0 && !activeProject)
|
||||||
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||||
}, [activeProject, data, projectId, projects, isOpen]);
|
}, [activeProject, data, projectId, projects, isOpen, prePopulateData]);
|
||||||
|
|
||||||
const calendarFetchKey = cycleId
|
const calendarFetchKey = cycleId
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
|
||||||
@ -228,6 +234,8 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
if (!data) await createIssue(payload);
|
if (!data) await createIssue(payload);
|
||||||
else await updateIssue(payload);
|
else await updateIssue(payload);
|
||||||
|
|
||||||
|
clearDraftIssueLocalStorage();
|
||||||
|
|
||||||
if (onSubmit) await onSubmit(payload);
|
if (onSubmit) await onSubmit(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import aiService from "services/ai.service";
|
import aiService from "services/ai.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
|
||||||
// components
|
// components
|
||||||
import { GptAssistantModal } from "components/core";
|
import { GptAssistantModal } from "components/core";
|
||||||
import { ParentIssuesListModal } from "components/issues";
|
import { ParentIssuesListModal } from "components/issues";
|
||||||
@ -62,11 +61,9 @@ export interface IssueFormProps {
|
|||||||
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
createMore: boolean;
|
createMore: boolean;
|
||||||
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
handleClose: () => void;
|
|
||||||
handleDiscardClose: () => void;
|
handleDiscardClose: () => void;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
setIsConfirmDiscardOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
handleFormDirty: (payload: Partial<IIssue> | null) => void;
|
handleFormDirty: (payload: Partial<IIssue> | null) => void;
|
||||||
fieldsToShow: (
|
fieldsToShow: (
|
||||||
| "project"
|
| "project"
|
||||||
@ -107,8 +104,6 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||||
|
|
||||||
const { setValue: setValueInLocalStorage } = useLocalStorage<any>("draftedIssue", null);
|
|
||||||
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -139,9 +134,11 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
state: getValues("state"),
|
state: getValues("state"),
|
||||||
priority: getValues("priority"),
|
priority: getValues("priority"),
|
||||||
assignees: getValues("assignees"),
|
assignees: getValues("assignees"),
|
||||||
target_date: getValues("target_date"),
|
|
||||||
labels: getValues("labels"),
|
labels: getValues("labels"),
|
||||||
|
start_date: getValues("start_date"),
|
||||||
|
target_date: getValues("target_date"),
|
||||||
project: getValues("project"),
|
project: getValues("project"),
|
||||||
|
parent: getValues("parent"),
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -571,8 +568,6 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const data = JSON.stringify(getValues());
|
|
||||||
setValueInLocalStorage(data);
|
|
||||||
handleDiscardClose();
|
handleDiscardClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -19,6 +19,7 @@ import useInboxView from "hooks/use-inbox-view";
|
|||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
import useProjects from "hooks/use-projects";
|
import useProjects from "hooks/use-projects";
|
||||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import { IssueForm, ConfirmIssueDiscard } from "components/issues";
|
import { IssueForm, ConfirmIssueDiscard } from "components/issues";
|
||||||
// types
|
// types
|
||||||
@ -92,10 +93,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
|
|
||||||
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||||
|
|
||||||
|
const { setValue: setValueInLocalStorage, clearValue: clearLocalStorageValue } =
|
||||||
|
useLocalStorage<any>("draftedIssue", {});
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
|
||||||
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
|
|
||||||
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
|
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
|
||||||
prePopulateData = {
|
prePopulateData = {
|
||||||
...prePopulateData,
|
...prePopulateData,
|
||||||
@ -103,6 +105,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
|
if (!showConfirmDiscard) handleClose();
|
||||||
|
if (formDirtyState === null) return setActiveProject(null);
|
||||||
|
const data = JSON.stringify(formDirtyState);
|
||||||
|
setValueInLocalStorage(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDiscardClose = () => {
|
||||||
if (formDirtyState !== null) {
|
if (formDirtyState !== null) {
|
||||||
setShowConfirmDiscard(true);
|
setShowConfirmDiscard(true);
|
||||||
} else {
|
} else {
|
||||||
@ -111,11 +120,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDiscardClose = () => {
|
|
||||||
handleClose();
|
|
||||||
setActiveProject(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFormDirty = (data: any) => {
|
const handleFormDirty = (data: any) => {
|
||||||
setFormDirtyState(data);
|
setFormDirtyState(data);
|
||||||
};
|
};
|
||||||
@ -397,6 +401,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
setActiveProject(null);
|
setActiveProject(null);
|
||||||
setFormDirtyState(null);
|
setFormDirtyState(null);
|
||||||
setShowConfirmDiscard(false);
|
setShowConfirmDiscard(false);
|
||||||
|
clearLocalStorageValue();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -431,9 +436,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
initialData={data ?? prePopulateData}
|
initialData={data ?? prePopulateData}
|
||||||
createMore={createMore}
|
createMore={createMore}
|
||||||
setCreateMore={setCreateMore}
|
setCreateMore={setCreateMore}
|
||||||
handleClose={onClose}
|
|
||||||
handleDiscardClose={onDiscardClose}
|
handleDiscardClose={onDiscardClose}
|
||||||
setIsConfirmDiscardOpen={setShowConfirmDiscard}
|
|
||||||
projectId={activeProject ?? ""}
|
projectId={activeProject ?? ""}
|
||||||
setActiveProject={setActiveProject}
|
setActiveProject={setActiveProject}
|
||||||
status={data ? true : false}
|
status={data ? true : false}
|
||||||
|
@ -73,7 +73,7 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
...selectedIssues.map((issue) => ({
|
...selectedIssues.map((issue) => ({
|
||||||
issue: issueId as string,
|
issue: issueId as string,
|
||||||
relation_type: "blocked_by" as const,
|
relation_type: "blocked_by" as const,
|
||||||
related_issue_detail: issue.blocked_issue_detail,
|
issue_detail: issue.blocked_issue_detail,
|
||||||
related_issue: issue.blocked_issue_detail.id,
|
related_issue: issue.blocked_issue_detail.id,
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
@ -111,17 +111,17 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
{blockedByIssue && blockedByIssue.length > 0
|
{blockedByIssue && blockedByIssue.length > 0
|
||||||
? blockedByIssue.map((relation) => (
|
? blockedByIssue.map((relation) => (
|
||||||
<div
|
<div
|
||||||
key={relation.related_issue_detail?.id}
|
key={relation?.id}
|
||||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${relation.related_issue_detail?.project_detail.id}/issues/${relation.related_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${relation.issue_detail?.project_detail.id}/issues/${relation.issue_detail?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<BlockedIcon height={10} width={10} />
|
<BlockedIcon height={10} width={10} />
|
||||||
{`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`}
|
{`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -81,6 +81,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
related_issue_detail: issue.blocker_issue_detail,
|
related_issue_detail: issue.blocker_issue_detail,
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
|
relation: "blocking",
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
submitChanges({
|
submitChanges({
|
||||||
@ -89,7 +90,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
...(response ?? []).map((i: any) => ({
|
...(response ?? []).map((i: any) => ({
|
||||||
id: i.id,
|
id: i.id,
|
||||||
relation_type: i.relation_type,
|
relation_type: i.relation_type,
|
||||||
issue_detail: i.related_issue_detail,
|
issue_detail: i.issue_detail,
|
||||||
issue: i.related_issue,
|
issue: i.related_issue,
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
@ -118,7 +119,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
{blockerIssue && blockerIssue.length > 0
|
{blockerIssue && blockerIssue.length > 0
|
||||||
? blockerIssue.map((relation) => (
|
? blockerIssue.map((relation) => (
|
||||||
<div
|
<div
|
||||||
key={relation.issue_detail?.id}
|
key={relation.id}
|
||||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -134,9 +135,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedBlockers = blockerIssue.filter(
|
const updatedBlockers = blockerIssue.filter((i) => i.id !== relation.id);
|
||||||
(i) => i.issue_detail?.id !== relation.issue_detail?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
submitChanges({
|
submitChanges({
|
||||||
issue_relations: updatedBlockers,
|
issue_relations: updatedBlockers,
|
||||||
|
@ -18,7 +18,7 @@ import { BlockeIssueDetail, IIssue, ISearchIssueResponse } from "types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId?: string;
|
issueId?: string;
|
||||||
submitChanges: (formData: Partial<IIssue>) => void;
|
submitChanges: (formData?: Partial<IIssue>) => void;
|
||||||
watch: UseFormWatch<IIssue>;
|
watch: UseFormWatch<IIssue>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
@ -69,7 +69,7 @@ export const SidebarDuplicateSelect: React.FC<Props> = (props) => {
|
|||||||
related_list: [
|
related_list: [
|
||||||
...selectedIssues.map((issue) => ({
|
...selectedIssues.map((issue) => ({
|
||||||
issue: issueId as string,
|
issue: issueId as string,
|
||||||
related_issue_detail: issue.blocker_issue_detail,
|
issue_detail: issue.blocker_issue_detail,
|
||||||
related_issue: issue.blocker_issue_detail.id,
|
related_issue: issue.blocker_issue_detail.id,
|
||||||
relation_type: "duplicate" as const,
|
relation_type: "duplicate" as const,
|
||||||
})),
|
})),
|
||||||
@ -90,7 +90,7 @@ export const SidebarDuplicateSelect: React.FC<Props> = (props) => {
|
|||||||
?.filter((i) => i.relation_type === "duplicate")
|
?.filter((i) => i.relation_type === "duplicate")
|
||||||
.map((i) => ({
|
.map((i) => ({
|
||||||
...i,
|
...i,
|
||||||
related_issue_detail: i.issue_detail,
|
issue_detail: i.issue_detail,
|
||||||
related_issue: i.issue_detail?.id,
|
related_issue: i.issue_detail?.id,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
@ -114,39 +114,35 @@ export const SidebarDuplicateSelect: React.FC<Props> = (props) => {
|
|||||||
{duplicateIssuesRelation && duplicateIssuesRelation.length > 0
|
{duplicateIssuesRelation && duplicateIssuesRelation.length > 0
|
||||||
? duplicateIssuesRelation.map((relation) => (
|
? duplicateIssuesRelation.map((relation) => (
|
||||||
<div
|
<div
|
||||||
key={relation.related_issue_detail?.id}
|
key={relation.issue_detail?.id}
|
||||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${relation.related_issue_detail?.project_detail.id}/issues/${relation.related_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${relation.issue_detail?.project_detail.id}/issues/${relation.issue_detail?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<BlockerIcon height={10} width={10} />
|
<BlockerIcon height={10} width={10} />
|
||||||
{`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`}
|
{`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedBlockers = duplicateIssuesRelation.filter(
|
|
||||||
(i) => i.related_issue_detail?.id !== relation.related_issue_detail?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
submitChanges({
|
|
||||||
related_issues: updatedBlockers,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
issuesService.deleteIssueRelation(
|
issuesService
|
||||||
workspaceSlug as string,
|
.deleteIssueRelation(
|
||||||
projectId as string,
|
workspaceSlug as string,
|
||||||
issueId as string,
|
projectId as string,
|
||||||
relation.id,
|
issueId as string,
|
||||||
user
|
relation.id,
|
||||||
);
|
user
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
submitChanges();
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X className="h-2 w-2" />
|
<X className="h-2 w-2" />
|
||||||
|
@ -18,7 +18,7 @@ import { BlockeIssueDetail, IIssue, ISearchIssueResponse } from "types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId?: string;
|
issueId?: string;
|
||||||
submitChanges: (formData: Partial<IIssue>) => void;
|
submitChanges: (formData?: Partial<IIssue>) => void;
|
||||||
watch: UseFormWatch<IIssue>;
|
watch: UseFormWatch<IIssue>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
@ -69,7 +69,7 @@ export const SidebarRelatesSelect: React.FC<Props> = (props) => {
|
|||||||
related_list: [
|
related_list: [
|
||||||
...selectedIssues.map((issue) => ({
|
...selectedIssues.map((issue) => ({
|
||||||
issue: issueId as string,
|
issue: issueId as string,
|
||||||
related_issue_detail: issue.blocker_issue_detail,
|
issue_detail: issue.blocker_issue_detail,
|
||||||
related_issue: issue.blocker_issue_detail.id,
|
related_issue: issue.blocker_issue_detail.id,
|
||||||
relation_type: "relates_to" as const,
|
relation_type: "relates_to" as const,
|
||||||
})),
|
})),
|
||||||
@ -90,7 +90,7 @@ export const SidebarRelatesSelect: React.FC<Props> = (props) => {
|
|||||||
?.filter((i) => i.relation_type === "relates_to")
|
?.filter((i) => i.relation_type === "relates_to")
|
||||||
.map((i) => ({
|
.map((i) => ({
|
||||||
...i,
|
...i,
|
||||||
related_issue_detail: i.issue_detail,
|
issue_detail: i.issue_detail,
|
||||||
related_issue: i.issue_detail?.id,
|
related_issue: i.issue_detail?.id,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
@ -114,39 +114,35 @@ export const SidebarRelatesSelect: React.FC<Props> = (props) => {
|
|||||||
{relatedToIssueRelation && relatedToIssueRelation.length > 0
|
{relatedToIssueRelation && relatedToIssueRelation.length > 0
|
||||||
? relatedToIssueRelation.map((relation) => (
|
? relatedToIssueRelation.map((relation) => (
|
||||||
<div
|
<div
|
||||||
key={relation.related_issue_detail?.id}
|
key={relation.issue_detail?.id}
|
||||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${relation.related_issue_detail?.project_detail.id}/issues/${relation.related_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${relation.issue_detail?.project_detail.id}/issues/${relation.issue_detail?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<BlockerIcon height={10} width={10} />
|
<BlockerIcon height={10} width={10} />
|
||||||
{`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`}
|
{`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedBlockers = relatedToIssueRelation.filter(
|
|
||||||
(i) => i.related_issue_detail?.id !== relation.related_issue_detail?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
submitChanges({
|
|
||||||
related_issues: updatedBlockers,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
issuesService.deleteIssueRelation(
|
issuesService
|
||||||
workspaceSlug as string,
|
.deleteIssueRelation(
|
||||||
projectId as string,
|
workspaceSlug as string,
|
||||||
issueId as string,
|
projectId as string,
|
||||||
relation.id,
|
issueId as string,
|
||||||
user
|
relation.id,
|
||||||
);
|
user
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
submitChanges();
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X className="h-2 w-2" />
|
<X className="h-2 w-2" />
|
||||||
|
@ -509,17 +509,14 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
<SidebarDuplicateSelect
|
<SidebarDuplicateSelect
|
||||||
issueId={issueId as string}
|
issueId={issueId as string}
|
||||||
submitChanges={(data: any) => {
|
submitChanges={(data: any) => {
|
||||||
mutate<IIssue>(
|
if (!data) return mutate(ISSUE_DETAILS(issueId as string));
|
||||||
ISSUE_DETAILS(issueId as string),
|
mutate<IIssue>(ISSUE_DETAILS(issueId as string), (prevData) => {
|
||||||
(prevData) => {
|
if (!prevData) return prevData;
|
||||||
if (!prevData) return prevData;
|
return {
|
||||||
return {
|
...prevData,
|
||||||
...prevData,
|
...data,
|
||||||
...data,
|
};
|
||||||
};
|
});
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
@ -529,17 +526,14 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
<SidebarRelatesSelect
|
<SidebarRelatesSelect
|
||||||
issueId={issueId as string}
|
issueId={issueId as string}
|
||||||
submitChanges={(data: any) => {
|
submitChanges={(data: any) => {
|
||||||
mutate<IIssue>(
|
if (!data) return mutate(ISSUE_DETAILS(issueId as string));
|
||||||
ISSUE_DETAILS(issueId as string),
|
mutate<IIssue>(ISSUE_DETAILS(issueId as string), (prevData) => {
|
||||||
(prevData) => {
|
if (!prevData) return prevData;
|
||||||
if (!prevData) return prevData;
|
return {
|
||||||
return {
|
...prevData,
|
||||||
...prevData,
|
...data,
|
||||||
...data,
|
};
|
||||||
};
|
});
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
|
2
web/components/ui/buttons/type.d.ts
vendored
2
web/components/ui/buttons/type.d.ts
vendored
@ -1,7 +1,7 @@
|
|||||||
export type ButtonProps = {
|
export type ButtonProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: () => void;
|
onClick?: (e: any) => void;
|
||||||
type?: "button" | "submit" | "reset";
|
type?: "button" | "submit" | "reset";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
|
import { CopyPlus } from "lucide-react";
|
||||||
import { Icon, Tooltip } from "components/ui";
|
import { Icon, Tooltip } from "components/ui";
|
||||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
@ -117,7 +118,7 @@ const activityDetails: {
|
|||||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
blocks: {
|
blocked_by: {
|
||||||
message: (activity) => (
|
message: (activity) => (
|
||||||
<>
|
<>
|
||||||
{activity.old_value === ""
|
{activity.old_value === ""
|
||||||
@ -132,6 +133,36 @@ const activityDetails: {
|
|||||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
duplicate: {
|
||||||
|
message: (activity) => (
|
||||||
|
<>
|
||||||
|
{activity.old_value === ""
|
||||||
|
? "marked this issue as duplicate of "
|
||||||
|
: "removed this issue as a duplicate of "}
|
||||||
|
<span className="font-medium text-custom-text-100">
|
||||||
|
{activity.verb === "created" ? activity.new_value : activity.old_value}
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
icon: <CopyPlus size={12} color="#6b7280" />,
|
||||||
|
},
|
||||||
|
|
||||||
|
relates_to: {
|
||||||
|
message: (activity) => (
|
||||||
|
<>
|
||||||
|
{activity.old_value === ""
|
||||||
|
? "marked that this issue relates to "
|
||||||
|
: "removed the relation from "}
|
||||||
|
<span className="font-medium text-custom-text-100">
|
||||||
|
{activity.old_value === "" ? activity.new_value : activity.old_value}
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
|
||||||
|
},
|
||||||
|
|
||||||
cycles: {
|
cycles: {
|
||||||
message: (activity) => (
|
message: (activity) => (
|
||||||
<>
|
<>
|
||||||
|
@ -351,25 +351,23 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
|||||||
{blockedIssue &&
|
{blockedIssue &&
|
||||||
blockedIssue.map((issue) => (
|
blockedIssue.map((issue) => (
|
||||||
<div
|
<div
|
||||||
key={issue.related_issue_detail?.id}
|
key={issue.issue_detail?.id}
|
||||||
className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${issue.related_issue_detail?.project_detail.id}/issues/${issue.related_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${issue.issue_detail?.project_detail.id}/issues/${issue.issue_detail?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<BlockedIcon height={10} width={10} />
|
<BlockedIcon height={10} width={10} />
|
||||||
{`${issue?.related_issue_detail?.project_detail?.identifier}-${issue?.related_issue_detail?.sequence_id}`}
|
{`${issue?.issue_detail?.project_detail?.identifier}-${issue?.issue_detail?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="duration-300"
|
className="duration-300"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedBlocked = blockedIssue.filter(
|
const updatedBlocked = blockedIssue.filter((i) => i?.id !== issue?.id);
|
||||||
(i) => i.related_issue_detail?.id !== issue.related_issue_detail?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
|
@ -17,7 +17,10 @@ export const WorkspaceSidebarQuickAction = () => {
|
|||||||
|
|
||||||
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
|
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
|
||||||
|
|
||||||
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", null);
|
const { storedValue, clearValue } = useLocalStorage<any>(
|
||||||
|
"draftedIssue",
|
||||||
|
JSON.stringify(undefined)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -30,18 +33,7 @@ export const WorkspaceSidebarQuickAction = () => {
|
|||||||
clearValue();
|
clearValue();
|
||||||
setIsDraftIssueModalOpen(false);
|
setIsDraftIssueModalOpen(false);
|
||||||
}}
|
}}
|
||||||
fieldsToShow={[
|
fieldsToShow={["all"]}
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"label",
|
|
||||||
"assignee",
|
|
||||||
"priority",
|
|
||||||
"dueDate",
|
|
||||||
"priority",
|
|
||||||
"state",
|
|
||||||
"startDate",
|
|
||||||
"project",
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -50,7 +42,7 @@ export const WorkspaceSidebarQuickAction = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 ${
|
className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 group ${
|
||||||
store?.theme?.sidebarCollapsed
|
store?.theme?.sidebarCollapsed
|
||||||
? "px-2 hover:bg-custom-sidebar-background-80"
|
? "px-2 hover:bg-custom-sidebar-background-80"
|
||||||
: "px-3 shadow border-[0.5px] border-custom-border-300"
|
: "px-3 shadow border-[0.5px] border-custom-border-300"
|
||||||
|
@ -154,6 +154,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
relation_type: "duplicate" | "relates_to" | "blocked_by";
|
relation_type: "duplicate" | "relates_to" | "blocked_by";
|
||||||
related_issue: string;
|
related_issue: string;
|
||||||
}>;
|
}>;
|
||||||
|
relation?: "blocking" | null;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return this.post(
|
return this.post(
|
||||||
@ -658,7 +659,12 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDraftIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
async deleteDraftIssue(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
user: ICurrentUserResponse
|
||||||
|
): Promise<any> {
|
||||||
return this.delete(
|
return this.delete(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
|
||||||
)
|
)
|
||||||
|
189
web/store/draft-issue.ts
Normal file
189
web/store/draft-issue.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// mobx
|
||||||
|
import { action, observable, runInAction, makeAutoObservable } from "mobx";
|
||||||
|
// services
|
||||||
|
import issueService from "services/issues.service";
|
||||||
|
// types
|
||||||
|
import type { ICurrentUserResponse, IIssue } from "types";
|
||||||
|
|
||||||
|
class DraftIssuesStore {
|
||||||
|
issues: { [key: string]: IIssue } = {};
|
||||||
|
isIssuesLoading: boolean = false;
|
||||||
|
rootStore: any | null = null;
|
||||||
|
|
||||||
|
constructor(_rootStore: any | null = null) {
|
||||||
|
makeAutoObservable(this, {
|
||||||
|
issues: observable.ref,
|
||||||
|
isIssuesLoading: observable.ref,
|
||||||
|
rootStore: observable.ref,
|
||||||
|
loadDraftIssues: action,
|
||||||
|
getIssueById: action,
|
||||||
|
createDraftIssue: action,
|
||||||
|
updateDraftIssue: action,
|
||||||
|
deleteDraftIssue: action,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Fetch all draft issues of a project and hydrate issues field
|
||||||
|
*/
|
||||||
|
|
||||||
|
loadDraftIssues = async (workspaceSlug: string, projectId: string, params?: any) => {
|
||||||
|
this.isIssuesLoading = true;
|
||||||
|
try {
|
||||||
|
const issuesResponse = await issueService.getDraftIssues(workspaceSlug, projectId, params);
|
||||||
|
|
||||||
|
const issues = Array.isArray(issuesResponse) ? { allIssues: issuesResponse } : issuesResponse;
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.issues = issues;
|
||||||
|
this.isIssuesLoading = false;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.isIssuesLoading = false;
|
||||||
|
console.error("Fetching issues error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Fetch a single draft issue by id and hydrate issues field
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @param projectId
|
||||||
|
* @param issueId
|
||||||
|
* @returns {IIssue}
|
||||||
|
*/
|
||||||
|
|
||||||
|
getIssueById = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string
|
||||||
|
): Promise<IIssue> => {
|
||||||
|
if (this.issues[issueId]) return this.issues[issueId];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const issueResponse: IIssue = await issueService.getDraftIssueById(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueId
|
||||||
|
);
|
||||||
|
|
||||||
|
const issues = {
|
||||||
|
...this.issues,
|
||||||
|
[issueId]: { ...issueResponse },
|
||||||
|
};
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.issues = issues;
|
||||||
|
});
|
||||||
|
|
||||||
|
return issueResponse;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Create a new draft issue and hydrate issues field
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @param projectId
|
||||||
|
* @param issueForm
|
||||||
|
* @param user
|
||||||
|
* @returns {IIssue}
|
||||||
|
*/
|
||||||
|
|
||||||
|
createDraftIssue = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueForm: IIssue,
|
||||||
|
user: ICurrentUserResponse
|
||||||
|
): Promise<IIssue> => {
|
||||||
|
try {
|
||||||
|
const issueResponse = await issueService.createDraftIssue(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueForm,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
const issues = {
|
||||||
|
...this.issues,
|
||||||
|
[issueResponse.id]: { ...issueResponse },
|
||||||
|
};
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.issues = issues;
|
||||||
|
});
|
||||||
|
return issueResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Creating issue error", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDraftIssue = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
issueForm: Partial<IIssue>,
|
||||||
|
user: ICurrentUserResponse
|
||||||
|
) => {
|
||||||
|
// keep a copy of the issue in the store
|
||||||
|
const originalIssue = { ...this.issues[issueId] };
|
||||||
|
|
||||||
|
// immediately update the issue in the store
|
||||||
|
const updatedIssue = { ...this.issues[issueId], ...issueForm };
|
||||||
|
if (updatedIssue.assignees_list) updatedIssue.assignees = updatedIssue.assignees_list;
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.issues[issueId] = { ...updatedIssue };
|
||||||
|
});
|
||||||
|
|
||||||
|
// make a patch request to update the issue
|
||||||
|
const issueResponse: IIssue = await issueService.updateDraftIssue(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueId,
|
||||||
|
issueForm,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedIssues = { ...this.issues };
|
||||||
|
updatedIssues[issueId] = { ...issueResponse };
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.issues = updatedIssues;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// if there is an error, revert the changes
|
||||||
|
runInAction(() => {
|
||||||
|
this.issues[issueId] = originalIssue;
|
||||||
|
});
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteDraftIssue = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
user: ICurrentUserResponse
|
||||||
|
) => {
|
||||||
|
const issues = { ...this.issues };
|
||||||
|
delete issues[issueId];
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.issues = issues;
|
||||||
|
});
|
||||||
|
|
||||||
|
issueService.deleteDraftIssue(workspaceSlug, projectId, issueId, user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Deleting issue error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DraftIssuesStore;
|
@ -6,6 +6,7 @@ import ThemeStore from "./theme";
|
|||||||
import ProjectStore, { IProjectStore } from "./project";
|
import ProjectStore, { IProjectStore } from "./project";
|
||||||
import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
|
import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
|
||||||
import IssuesStore from "./issues";
|
import IssuesStore from "./issues";
|
||||||
|
import DraftIssuesStore from "./draft-issue";
|
||||||
|
|
||||||
enableStaticRendering(typeof window === "undefined");
|
enableStaticRendering(typeof window === "undefined");
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ export class RootStore {
|
|||||||
project: IProjectStore;
|
project: IProjectStore;
|
||||||
projectPublish: IProjectPublishStore;
|
projectPublish: IProjectPublishStore;
|
||||||
issues: IssuesStore;
|
issues: IssuesStore;
|
||||||
|
draftIssuesStore: DraftIssuesStore;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.user = new UserStore(this);
|
this.user = new UserStore(this);
|
||||||
@ -22,5 +24,6 @@ export class RootStore {
|
|||||||
this.project = new ProjectStore(this);
|
this.project = new ProjectStore(this);
|
||||||
this.projectPublish = new ProjectPublishStore(this);
|
this.projectPublish = new ProjectPublishStore(this);
|
||||||
this.issues = new IssuesStore(this);
|
this.issues = new IssuesStore(this);
|
||||||
|
this.draftIssuesStore = new DraftIssuesStore(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
web/types/issues.d.ts
vendored
20
web/types/issues.d.ts
vendored
@ -76,8 +76,10 @@ export type IssueRelationType = "duplicate" | "relates_to" | "blocked_by";
|
|||||||
export interface IssueRelation {
|
export interface IssueRelation {
|
||||||
id: string;
|
id: string;
|
||||||
issue: string;
|
issue: string;
|
||||||
related_issue: string;
|
issue_detail: BlockeIssueDetail;
|
||||||
relation_type: IssueRelationType;
|
relation_type: IssueRelationType;
|
||||||
|
related_issue: string;
|
||||||
|
relation: "blocking" | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssue {
|
export interface IIssue {
|
||||||
@ -87,20 +89,8 @@ export interface IIssue {
|
|||||||
assignees_list: string[];
|
assignees_list: string[];
|
||||||
attachment_count: number;
|
attachment_count: number;
|
||||||
attachments: any[];
|
attachments: any[];
|
||||||
issue_relations: {
|
issue_relations: IssueRelation[];
|
||||||
id: string;
|
related_issues: IssueRelation[];
|
||||||
issue: string;
|
|
||||||
issue_detail: BlockeIssueDetail;
|
|
||||||
relation_type: IssueRelationType;
|
|
||||||
related_issue: string;
|
|
||||||
}[];
|
|
||||||
related_issues: {
|
|
||||||
id: string;
|
|
||||||
issue: string;
|
|
||||||
related_issue_detail: BlockeIssueDetail;
|
|
||||||
relation_type: IssueRelationType;
|
|
||||||
related_issue: string;
|
|
||||||
}[];
|
|
||||||
bridge_id?: string | null;
|
bridge_id?: string | null;
|
||||||
completed_at: Date;
|
completed_at: Date;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user