Compare commits

...

10 Commits

Author SHA1 Message Date
Aaryan Khandelwal 2d665db5b5 style: remove unnecessary bottom padding from the rich text editor 2024-11-08 20:00:56 +05:30
sriram veeraghanta 389ee74ff5 chore: migrations fixes and cleanup 2024-11-08 19:23:42 +05:30
Akshita Goyal 0cd36b854e fix: intake loading (#5966)
* fix: intake loading

* fix: image upload in space
2024-11-08 17:17:15 +05:30
Bavisetti Narayan 1d314dd25f fix: renamed inbox to intake (#5967)
* feat: intake

* chore: intake model migration changes

* dev: update dummy data

* dev: add duplicate apis for inbox

* dev: fix external apis

* fix: external apis

* chore: migration file changes

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-11-08 17:10:24 +05:30
rahulramesha 1743717351 fix related to activity (#5972) 2024-11-08 17:09:49 +05:30
Satish Gandham acba451803 [WEB-2706] fix: Add fallback when db initialisation fails (#5973)
* Add fallback when db initialization fails

* add checks for instance.exec

* chore: convert issue boolean fields to actual boolean value.

* change instance exec code

* sync issue to local db when inbox issue is accepted and draft issue is moved to project

* chore: added project and workspace keys

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-11-08 17:09:26 +05:30
Aaryan Khandelwal 2193e8c79c fix: editor user config (#5974) 2024-11-08 13:30:06 +05:30
Anmol Singh Bhatia 4c6ab984c3 [WEB-2742] chore: issue link ui revamp (#5971)
* chore-issue-link-ui

* chore: issue link ui revamp
2024-11-07 19:24:15 +05:30
Prateek Shourya 7574206a41 [WEB-2554] improvement: dashboard sidebar list items. (#5970) 2024-11-07 15:31:28 +05:30
Anmol Singh Bhatia eebc327b10 chore: app sidebar behaviour (#5964) 2024-11-06 18:36:23 +05:30
80 changed files with 910 additions and 834 deletions
+1 -1
View File
@@ -18,4 +18,4 @@ from .module import (
ModuleIssueSerializer,
ModuleLiteSerializer,
)
from .inbox import InboxIssueSerializer
from .intake import IntakeIssueSerializer
@@ -1,15 +1,17 @@
# Module improts
from .base import BaseSerializer
from .issue import IssueExpandSerializer
from plane.db.models import InboxIssue
from plane.db.models import IntakeIssue
from rest_framework import serializers
class InboxIssueSerializer(BaseSerializer):
class IntakeIssueSerializer(BaseSerializer):
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
inbox = serializers.UUIDField(source="intake.id", read_only=True)
class Meta:
model = InboxIssue
model = IntakeIssue
fields = "__all__"
read_only_fields = [
"id",
@@ -20,6 +20,7 @@ class ProjectSerializer(BaseSerializer):
member_role = serializers.IntegerField(read_only=True)
is_deployed = serializers.BooleanField(read_only=True)
cover_image_url = serializers.CharField(read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
class Meta:
model = Project
+2 -2
View File
@@ -3,7 +3,7 @@ from .state import urlpatterns as state_patterns
from .issue import urlpatterns as issue_patterns
from .cycle import urlpatterns as cycle_patterns
from .module import urlpatterns as module_patterns
from .inbox import urlpatterns as inbox_patterns
from .intake import urlpatterns as intake_patterns
from .member import urlpatterns as member_patterns
urlpatterns = [
@@ -12,6 +12,6 @@ urlpatterns = [
*issue_patterns,
*cycle_patterns,
*module_patterns,
*inbox_patterns,
*intake_patterns,
*member_patterns,
]
-17
View File
@@ -1,17 +0,0 @@
from django.urls import path
from plane.api.views import InboxIssueAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
InboxIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
InboxIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
]
+27
View File
@@ -0,0 +1,27 @@
from django.urls import path
from plane.api.views import IntakeIssueAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
IntakeIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
IntakeIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
IntakeIssueAPIEndpoint.as_view(),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:issue_id>/",
IntakeIssueAPIEndpoint.as_view(),
name="intake-issue",
),
]
+1 -2
View File
@@ -27,5 +27,4 @@ from .module import (
from .member import ProjectMemberAPIEndpoint
from .inbox import InboxIssueAPIEndpoint
from .intake import IntakeIssueAPIEndpoint
@@ -14,12 +14,12 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
from plane.api.serializers import IntakeIssueSerializer, IssueSerializer
from plane.app.permissions import ProjectLitePermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import (
Inbox,
InboxIssue,
Intake,
IntakeIssue,
Issue,
Project,
ProjectMember,
@@ -29,10 +29,10 @@ from plane.db.models import (
from .base import BaseAPIView
class InboxIssueAPIEndpoint(BaseAPIView):
class IntakeIssueAPIEndpoint(BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to inbox issues.
`update` and `destroy` actions related to intake issues.
"""
@@ -40,15 +40,15 @@ class InboxIssueAPIEndpoint(BaseAPIView):
ProjectLitePermission,
]
serializer_class = InboxIssueSerializer
model = InboxIssue
serializer_class = IntakeIssueSerializer
model = IntakeIssue
filterset_fields = [
"status",
]
def get_queryset(self):
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
).first()
@@ -58,16 +58,16 @@ class InboxIssueAPIEndpoint(BaseAPIView):
pk=self.kwargs.get("project_id"),
)
if inbox is None and not project.inbox_view:
return InboxIssue.objects.none()
if intake is None and not project.intake_view:
return IntakeIssue.objects.none()
return (
InboxIssue.objects.filter(
IntakeIssue.objects.filter(
Q(snoozed_till__gte=timezone.now())
| Q(snoozed_till__isnull=True),
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
inbox_id=inbox.id,
intake_id=intake.id,
)
.select_related("issue", "workspace", "project")
.order_by(self.kwargs.get("order_by", "-created_at"))
@@ -75,22 +75,22 @@ class InboxIssueAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, issue_id=None):
if issue_id:
inbox_issue_queryset = self.get_queryset().get(issue_id=issue_id)
inbox_issue_data = InboxIssueSerializer(
inbox_issue_queryset,
intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
intake_issue_data = IntakeIssueSerializer(
intake_issue_queryset,
fields=self.fields,
expand=self.expand,
).data
return Response(
inbox_issue_data,
intake_issue_data,
status=status.HTTP_200_OK,
)
issue_queryset = self.get_queryset()
return self.paginate(
request=request,
queryset=(issue_queryset),
on_results=lambda inbox_issues: InboxIssueSerializer(
inbox_issues,
on_results=lambda intake_issues: IntakeIssueSerializer(
intake_issues,
many=True,
fields=self.fields,
expand=self.expand,
@@ -104,7 +104,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
@@ -113,11 +113,11 @@ class InboxIssueAPIEndpoint(BaseAPIView):
pk=project_id,
)
# Inbox view
if inbox is None and not project.inbox_view:
# Intake view
if intake is None and not project.intake_view:
return Response(
{
"error": "Inbox is not enabled for this project enable it through the project's api"
"error": "Intake is not enabled for this project enable it through the project's api"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -139,7 +139,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
state, _ = State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Inbox Issues",
description="Default state for managing all Intake Issues",
project_id=project_id,
color="#ff7700",
is_triage=True,
@@ -157,12 +157,12 @@ class InboxIssueAPIEndpoint(BaseAPIView):
state=state,
)
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox.id,
# create an intake issue
intake_issue = IntakeIssue.objects.create(
intake_id=intake.id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
source=request.data.get("source", "IN-APP"),
)
# Create an Issue Activity
issue_activity.delay(
@@ -173,32 +173,37 @@ class InboxIssueAPIEndpoint(BaseAPIView):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
inbox=str(inbox_issue.id),
intake=str(intake_issue.id),
)
serializer = InboxIssueSerializer(inbox_issue)
serializer = IntakeIssueSerializer(intake_issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, issue_id):
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# Inbox view
if inbox is None:
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
# Intake view
if intake is None and not project.intake_view:
return Response(
{
"error": "Inbox is not enabled for this project enable it through the project's api"
"error": "Intake is not enabled for this project enable it through the project's api"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the inbox issue
inbox_issue = InboxIssue.objects.get(
# Get the intake issue
intake_issue = IntakeIssue.objects.get(
issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox.id,
intake_id=intake.id,
)
# Get the project member
@@ -210,11 +215,11 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 5 and str(inbox_issue.created_by_id) != str(
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot edit inbox issues"},
{"error": "You cannot edit intake issues"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -283,7 +288,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
inbox=(inbox_issue.id),
intake=(intake_issue.id),
)
issue_serializer.save()
else:
@@ -291,13 +296,13 @@ class InboxIssueAPIEndpoint(BaseAPIView):
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Only project admins and members can edit inbox issue attributes
# Only project admins and members can edit intake issue attributes
if project_member.role > 15:
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
serializer = IntakeIssueSerializer(
intake_issue, data=request.data, partial=True
)
current_instance = json.dumps(
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid():
@@ -340,7 +345,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
# create a activity for status change
issue_activity.delay(
type="inbox.activity.created",
type="intake.activity.created",
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
@@ -351,7 +356,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
intake=str(intake_issue.id),
)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -360,12 +365,12 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
else:
return Response(
InboxIssueSerializer(inbox_issue).data,
IntakeIssueSerializer(intake_issue).data,
status=status.HTTP_200_OK,
)
def delete(self, request, slug, project_id, issue_id):
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
@@ -374,25 +379,25 @@ class InboxIssueAPIEndpoint(BaseAPIView):
pk=project_id,
)
# Inbox view
if inbox is None and not project.inbox_view:
# Intake view
if intake is None and not project.intake_view:
return Response(
{
"error": "Inbox is not enabled for this project enable it through the project's api"
"error": "Intake is not enabled for this project enable it through the project's api"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the inbox issue
inbox_issue = InboxIssue.objects.get(
# Get the intake issue
intake_issue = IntakeIssue.objects.get(
issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox.id,
intake_id=intake.id,
)
# Check the issue status
if inbox_issue.status in [-2, -1, 0, 2]:
if intake_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=issue_id
@@ -412,5 +417,5 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
issue.delete()
inbox_issue.delete()
intake_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+16 -8
View File
@@ -18,7 +18,7 @@ from plane.app.permissions import ProjectBasePermission
# Module imports
from plane.db.models import (
Cycle,
Inbox,
Intake,
IssueUserProperty,
Module,
Project,
@@ -285,6 +285,11 @@ class ProjectAPIEndpoint(BaseAPIView):
current_instance = json.dumps(
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
intake_view = request.data.get(
"inbox_view", request.data.get("intake_view", False)
)
if project.archived_at:
return Response(
{"error": "Archived project cannot be updated"},
@@ -293,21 +298,24 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer = ProjectSerializer(
project,
data={**request.data},
data={
**request.data,
"intake_view": intake_view,
},
context={"workspace_id": workspace.id},
partial=True,
)
if serializer.is_valid():
serializer.save()
if serializer.data["inbox_view"]:
inbox = Inbox.objects.filter(
if serializer.data["intake_view"]:
intake = Intake.objects.filter(
project=project,
is_default=True,
).first()
if not inbox:
Inbox.objects.create(
name=f"{project.name} Inbox",
if not intake:
Intake.objects.create(
name=f"{project.name} Intake",
project=project,
is_default=True,
)
@@ -316,7 +324,7 @@ class ProjectAPIEndpoint(BaseAPIView):
State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Inbox Issues",
description="Default state for managing all Intake Issues",
project_id=pk,
color="#ff7700",
is_triage=True,
+7 -8
View File
@@ -13,7 +13,6 @@ from .user import (
from .workspace import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
@@ -57,7 +56,7 @@ from .issue import (
IssueFlatSerializer,
IssueStateSerializer,
IssueLinkSerializer,
IssueInboxSerializer,
IssueIntakeSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
IssueSubscriberSerializer,
@@ -102,12 +101,12 @@ from .estimate import (
WorkspaceEstimateSerializer,
)
from .inbox import (
InboxSerializer,
InboxIssueSerializer,
IssueStateInboxSerializer,
InboxIssueLiteSerializer,
InboxIssueDetailSerializer,
from .intake import (
IntakeSerializer,
IntakeIssueSerializer,
IssueStateIntakeSerializer,
IntakeIssueLiteSerializer,
IntakeIssueDetailSerializer,
)
from .analytic import AnalyticViewSerializer
+5 -5
View File
@@ -60,7 +60,7 @@ class DynamicBaseSerializer(BaseSerializer):
CycleIssueSerializer,
IssueLiteSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer,
IntakeIssueLiteSerializer,
IssueReactionLiteSerializer,
IssueLinkLiteSerializer,
RelatedIssueSerializer,
@@ -84,8 +84,8 @@ class DynamicBaseSerializer(BaseSerializer):
"issue_cycle": CycleIssueSerializer,
"parent": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer,
"issue_intake": IntakeIssueLiteSerializer,
"issue_related": RelatedIssueSerializer,
"issue_inbox": InboxIssueLiteSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
"sub_issues": IssueLiteSerializer,
@@ -102,7 +102,7 @@ class DynamicBaseSerializer(BaseSerializer):
"labels",
"issue_cycle",
"issue_relation",
"issue_inbox",
"issue_intake",
"issue_reactions",
"issue_attachment",
"issue_link",
@@ -132,7 +132,7 @@ class DynamicBaseSerializer(BaseSerializer):
LabelSerializer,
CycleIssueSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer,
IntakeIssueLiteSerializer,
IssueLiteSerializer,
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
@@ -158,8 +158,8 @@ class DynamicBaseSerializer(BaseSerializer):
"issue_cycle": CycleIssueSerializer,
"parent": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer,
"issue_intake": IntakeIssueLiteSerializer,
"issue_related": RelatedIssueSerializer,
"issue_inbox": InboxIssueLiteSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_attachment": IssueAttachmentLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
@@ -4,22 +4,22 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import (
IssueInboxSerializer,
IssueIntakeSerializer,
LabelLiteSerializer,
IssueDetailSerializer,
)
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
from plane.db.models import Inbox, InboxIssue, Issue
from plane.db.models import Intake, IntakeIssue, Issue
class InboxSerializer(BaseSerializer):
class IntakeSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(source="project", read_only=True)
pending_issue_count = serializers.IntegerField(read_only=True)
class Meta:
model = Inbox
model = Intake
fields = "__all__"
read_only_fields = [
"project",
@@ -27,11 +27,11 @@ class InboxSerializer(BaseSerializer):
]
class InboxIssueSerializer(BaseSerializer):
issue = IssueInboxSerializer(read_only=True)
class IntakeIssueSerializer(BaseSerializer):
issue = IssueIntakeSerializer(read_only=True)
class Meta:
model = InboxIssue
model = IntakeIssue
fields = [
"id",
"status",
@@ -53,14 +53,14 @@ class InboxIssueSerializer(BaseSerializer):
return super().to_representation(instance)
class InboxIssueDetailSerializer(BaseSerializer):
class IntakeIssueDetailSerializer(BaseSerializer):
issue = IssueDetailSerializer(read_only=True)
duplicate_issue_detail = IssueInboxSerializer(
duplicate_issue_detail = IssueIntakeSerializer(
read_only=True, source="duplicate_to"
)
class Meta:
model = InboxIssue
model = IntakeIssue
fields = [
"id",
"status",
@@ -85,14 +85,14 @@ class InboxIssueDetailSerializer(BaseSerializer):
return super().to_representation(instance)
class InboxIssueLiteSerializer(BaseSerializer):
class IntakeIssueLiteSerializer(BaseSerializer):
class Meta:
model = InboxIssue
model = IntakeIssue
fields = ["id", "status", "duplicate_to", "snoozed_till", "source"]
read_only_fields = fields
class IssueStateInboxSerializer(BaseSerializer):
class IssueStateIntakeSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
label_details = LabelLiteSerializer(
@@ -102,7 +102,7 @@ class IssueStateInboxSerializer(BaseSerializer):
read_only=True, source="assignees", many=True
)
sub_issues_count = serializers.IntegerField(read_only=True)
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
issue_intake = IntakeIssueLiteSerializer(read_only=True, many=True)
class Meta:
model = Issue
+3 -1
View File
@@ -95,6 +95,8 @@ class IssueCreateSerializer(BaseSerializer):
write_only=True,
required=False,
)
project_id = serializers.UUIDField(source="project.id", read_only=True)
workspace_id = serializers.UUIDField(source="workspace.id", read_only=True)
class Meta:
model = Issue
@@ -643,7 +645,7 @@ class IssueStateSerializer(DynamicBaseSerializer):
fields = "__all__"
class IssueInboxSerializer(DynamicBaseSerializer):
class IssueIntakeSerializer(DynamicBaseSerializer):
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
@@ -12,6 +12,7 @@ class NotificationSerializer(BaseSerializer):
read_only=True, source="triggered_by"
)
is_inbox_issue = serializers.BooleanField(read_only=True)
is_intake_issue = serializers.BooleanField(read_only=True)
is_mentioned_notification = serializers.BooleanField(read_only=True)
class Meta:
@@ -22,6 +22,7 @@ class ProjectSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
class Meta:
model = Project
@@ -119,6 +120,7 @@ class ProjectListSerializer(DynamicBaseSerializer):
anchor = serializers.CharField(read_only=True)
members = serializers.SerializerMethodField()
cover_image_url = serializers.CharField(read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
def get_members(self, obj):
project_members = getattr(obj, "members_list", None)
@@ -9,8 +9,6 @@ from plane.db.models import (
User,
Workspace,
WorkspaceMember,
Team,
TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
@@ -99,57 +97,6 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
"updated_at",
]
class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer(
read_only=True, source="members", many=True
)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = Team
fields = "__all__"
read_only_fields = [
"workspace",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
def create(self, validated_data, **kwargs):
if "members" in validated_data:
members = validated_data.pop("members")
workspace = self.context["workspace"]
team = Team.objects.create(**validated_data, workspace=workspace)
team_members = [
TeamMember(member=member, team=team, workspace=workspace)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return team
team = Team.objects.create(**validated_data)
return team
def update(self, instance, validated_data):
if "members" in validated_data:
members = validated_data.pop("members")
TeamMember.objects.filter(team=instance).delete()
team_members = [
TeamMember(
member=member, team=instance, workspace=instance.workspace
)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return super().update(instance, validated_data)
return super().update(instance, validated_data)
class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
+2 -2
View File
@@ -5,7 +5,7 @@ from .cycle import urlpatterns as cycle_urls
from .dashboard import urlpatterns as dashboard_urls
from .estimate import urlpatterns as estimate_urls
from .external import urlpatterns as external_urls
from .inbox import urlpatterns as inbox_urls
from .intake import urlpatterns as intake_urls
from .issue import urlpatterns as issue_urls
from .module import urlpatterns as module_urls
from .notification import urlpatterns as notification_urls
@@ -25,7 +25,7 @@ urlpatterns = [
*dashboard_urls,
*estimate_urls,
*external_urls,
*inbox_urls,
*intake_urls,
*issue_urls,
*module_urls,
*notification_urls,
-53
View File
@@ -1,53 +0,0 @@
from django.urls import path
from plane.app.views import (
InboxViewSet,
InboxIssueViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
InboxViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
InboxViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
InboxIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
InboxIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
]
+95
View File
@@ -0,0 +1,95 @@
from django.urls import path
from plane.app.views import (
IntakeViewSet,
IntakeIssueViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/",
IntakeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/<uuid:pk>/",
IntakeViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
IntakeIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
IntakeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
IntakeViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
IntakeIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
]
-6
View File
@@ -7,7 +7,6 @@ from plane.app.views import (
ProjectMemberViewSet,
ProjectMemberUserEndpoint,
ProjectJoinEndpoint,
AddTeamToProjectEndpoint,
ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
@@ -116,11 +115,6 @@ urlpatterns = [
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
AddTeamToProjectEndpoint.as_view(),
name="projects",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(),
-23
View File
@@ -10,7 +10,6 @@ from plane.app.views import (
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
WorkSpaceAvailabilityCheckEndpoint,
TeamMemberViewSet,
UserLastProjectWithWorkspaceEndpoint,
WorkspaceThemeViewSet,
WorkspaceUserProfileStatsEndpoint,
@@ -127,28 +126,6 @@ urlpatterns = [
),
name="leave-workspace-members",
),
path(
"workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-team-members",
),
path(
"workspaces/<str:slug>/teams/<uuid:pk>/",
TeamMemberViewSet.as_view(
{
"put": "update",
"patch": "partial_update",
"delete": "destroy",
"get": "retrieve",
}
),
name="workspace-team-members",
),
path(
"users/last-visited-workspace/",
UserLastProjectWithWorkspaceEndpoint.as_view(),
+1 -3
View File
@@ -16,7 +16,6 @@ from .project.invite import (
from .project.member import (
ProjectMemberViewSet,
AddTeamToProjectEndpoint,
ProjectMemberUserEndpoint,
UserProjectRolesEndpoint,
)
@@ -49,7 +48,6 @@ from .workspace.favorite import (
from .workspace.member import (
WorkSpaceMemberViewSet,
TeamMemberViewSet,
WorkspaceMemberUserEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceMemberUserViewsEndpoint,
@@ -220,7 +218,7 @@ from .estimate.base import (
EstimatePointEndpoint,
)
from .inbox.base import InboxViewSet, InboxIssueViewSet
from .intake.base import IntakeViewSet, IntakeIssueViewSet
from .analytic.base import (
AnalyticsEndpoint,
+10 -8
View File
@@ -12,6 +12,7 @@ from django.views.decorators.gzip import gzip_page
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .. import BaseViewSet
from plane.app.serializers import (
@@ -39,6 +40,7 @@ from plane.utils.paginator import (
from plane.app.permissions import allow_permission, ROLE
class CycleIssueViewSet(BaseViewSet):
serializer_class = CycleIssueSerializer
model = CycleIssue
@@ -194,10 +196,10 @@ class CycleIssueViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -223,10 +225,10 @@ class CycleIssueViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -18,8 +18,8 @@ from rest_framework.response import Response
from ..base import BaseViewSet
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import (
Inbox,
InboxIssue,
Intake,
IntakeIssue,
Issue,
State,
IssueLink,
@@ -31,18 +31,18 @@ from plane.db.models import (
from plane.app.serializers import (
IssueCreateSerializer,
IssueSerializer,
InboxSerializer,
InboxIssueSerializer,
InboxIssueDetailSerializer,
IntakeSerializer,
IntakeIssueSerializer,
IntakeIssueDetailSerializer,
)
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activities_task import issue_activity
class InboxViewSet(BaseViewSet):
class IntakeViewSet(BaseViewSet):
serializer_class = InboxSerializer
model = Inbox
serializer_class = IntakeSerializer
model = Intake
def get_queryset(self):
return (
@@ -54,8 +54,8 @@ class InboxViewSet(BaseViewSet):
)
.annotate(
pending_issue_count=Count(
"issue_inbox",
filter=Q(issue_inbox__status=-2),
"issue_intake",
filter=Q(issue_intake__status=-2),
)
)
.select_related("workspace", "project")
@@ -63,9 +63,9 @@ class InboxViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def list(self, request, slug, project_id):
inbox = self.get_queryset().first()
intake = self.get_queryset().first()
return Response(
InboxSerializer(inbox).data,
IntakeSerializer(intake).data,
status=status.HTTP_200_OK,
)
@@ -75,26 +75,26 @@ class InboxViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def destroy(self, request, slug, project_id, pk):
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id, pk=pk
).first()
# Handle default inbox delete
if inbox.is_default:
# Handle default intake delete
if intake.is_default:
return Response(
{"error": "You cannot delete the default inbox"},
{"error": "You cannot delete the default intake"},
status=status.HTTP_400_BAD_REQUEST,
)
inbox.delete()
intake.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class InboxIssueViewSet(BaseViewSet):
class IntakeIssueViewSet(BaseViewSet):
serializer_class = InboxIssueSerializer
model = InboxIssue
serializer_class = IntakeIssueSerializer
model = IntakeIssue
filterset_fields = [
"status",
"statulls",
]
def get_queryset(self):
@@ -107,8 +107,8 @@ class InboxIssueViewSet(BaseViewSet):
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_inbox",
queryset=InboxIssue.objects.only(
"issue_intake",
queryset=IntakeIssue.objects.only(
"status", "duplicate_to", "snoozed_till", "source"
),
)
@@ -184,14 +184,14 @@ class InboxIssueViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(pk=project_id)
filters = issue_filters(request.GET, "GET", "issue__")
inbox_issue = (
InboxIssue.objects.filter(
inbox_id=inbox_id.id, project_id=project_id, **filters
intake_issue = (
IntakeIssue.objects.filter(
intake_id=intake_id.id, project_id=project_id, **filters
)
.select_related("issue")
.prefetch_related(
@@ -211,14 +211,14 @@ class InboxIssueViewSet(BaseViewSet):
)
)
).order_by(request.GET.get("order_by", "-issue__created_at"))
# inbox status filter
inbox_status = [
# Intake status filter
intake_status = [
item
for item in request.GET.get("status", "-2").split(",")
if item != "null"
]
if inbox_status:
inbox_issue = inbox_issue.filter(status__in=inbox_status)
if intake_status:
intake_issue = intake_issue.filter(status__in=intake_status)
if (
ProjectMember.objects.filter(
@@ -230,12 +230,12 @@ class InboxIssueViewSet(BaseViewSet):
).exists()
and not project.guest_view_all_features
):
inbox_issue = inbox_issue.filter(created_by=request.user)
intake_issue = intake_issue.filter(created_by=request.user)
return self.paginate(
request=request,
queryset=(inbox_issue),
on_results=lambda inbox_issues: InboxIssueSerializer(
inbox_issues,
queryset=(intake_issue),
on_results=lambda intake_issues: IntakeIssueSerializer(
intake_issues,
many=True,
).data,
)
@@ -261,16 +261,6 @@ class InboxIssueViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Inbox Issues",
project_id=project_id,
color="#ff7700",
is_triage=True,
)
# create an issue
project = Project.objects.get(pk=project_id)
serializer = IssueCreateSerializer(
@@ -283,15 +273,15 @@ class InboxIssueViewSet(BaseViewSet):
)
if serializer.is_valid():
serializer.save()
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox_id.id,
# create an intake issue
intake_issue = IntakeIssue.objects.create(
intake_id=intake_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "in-app"),
source=request.data.get("source", "IN-APP"),
)
# Create an Issue Activity
issue_activity.delay(
@@ -304,10 +294,10 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
intake=str(intake_issue.id),
)
inbox_issue = (
InboxIssue.objects.select_related("issue")
intake_issue = (
IntakeIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
@@ -339,12 +329,12 @@ class InboxIssueViewSet(BaseViewSet):
),
)
.get(
inbox_id=inbox_id.id,
intake_id=intake_id.id,
issue_id=serializer.data["id"],
project_id=project_id,
)
)
serializer = InboxIssueDetailSerializer(inbox_issue)
serializer = IntakeIssueDetailSerializer(intake_issue)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(
@@ -353,14 +343,14 @@ class InboxIssueViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
def partial_update(self, request, slug, project_id, pk):
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
inbox_issue = InboxIssue.objects.get(
intake_issue = IntakeIssue.objects.get(
issue_id=pk,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox_id,
intake_id=intake_id,
)
# Get the project member
project_member = ProjectMember.objects.get(
@@ -370,11 +360,11 @@ class InboxIssueViewSet(BaseViewSet):
is_active=True,
)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 5 and str(inbox_issue.created_by_id) != str(
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot edit inbox issues"},
{"error": "You cannot edit intake issues"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -405,7 +395,7 @@ class InboxIssueViewSet(BaseViewSet):
Value([], output_field=ArrayField(UUIDField())),
),
).get(
pk=inbox_issue.issue_id,
pk=intake_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
@@ -443,7 +433,7 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
intake=str(intake_issue.id),
)
issue_serializer.save()
else:
@@ -451,20 +441,20 @@ class InboxIssueViewSet(BaseViewSet):
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Only project admins and members can edit inbox issue attributes
# Only project admins and members can edit intake issue attributes
if project_member.role > 15:
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
serializer = IntakeIssueSerializer(
intake_issue, data=request.data, partial=True
)
current_instance = json.dumps(
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid():
serializer.save()
# Update the issue state if the issue is rejected or marked as duplicate
if serializer.data["status"] in [-1, 2]:
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
pk=intake_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
@@ -480,7 +470,7 @@ class InboxIssueViewSet(BaseViewSet):
# Update the issue state if it is accepted
if serializer.data["status"] in [1]:
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
pk=intake_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
@@ -498,7 +488,7 @@ class InboxIssueViewSet(BaseViewSet):
issue.save()
# create a activity for status change
issue_activity.delay(
type="inbox.activity.created",
type="intake.activity.created",
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
@@ -509,11 +499,11 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
inbox=(inbox_issue.id),
intake=(intake_issue.id),
)
inbox_issue = (
InboxIssue.objects.select_related("issue")
intake_issue = (
IntakeIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
@@ -547,18 +537,18 @@ class InboxIssueViewSet(BaseViewSet):
),
)
.get(
inbox_id=inbox_id.id,
intake_id=intake_id.id,
issue_id=pk,
project_id=project_id,
)
)
serializer = InboxIssueDetailSerializer(inbox_issue).data
serializer = IntakeIssueDetailSerializer(intake_issue).data
return Response(serializer, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
serializer = InboxIssueDetailSerializer(inbox_issue).data
serializer = IntakeIssueDetailSerializer(intake_issue).data
return Response(serializer, status=status.HTTP_200_OK)
@allow_permission(
@@ -571,12 +561,12 @@ class InboxIssueViewSet(BaseViewSet):
model=Issue,
)
def retrieve(self, request, slug, project_id, pk):
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(pk=project_id)
inbox_issue = (
InboxIssue.objects.select_related("issue")
intake_issue = (
IntakeIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
@@ -605,7 +595,7 @@ class InboxIssueViewSet(BaseViewSet):
Value([], output_field=ArrayField(UUIDField())),
),
)
.get(inbox_id=inbox_id.id, issue_id=pk, project_id=project_id)
.get(intake_id=intake_id.id, issue_id=pk, project_id=project_id)
)
if (
ProjectMember.objects.filter(
@@ -616,13 +606,13 @@ class InboxIssueViewSet(BaseViewSet):
is_active=True,
).exists()
and not project.guest_view_all_features
and not inbox_issue.created_by == request.user
and not intake_issue.created_by == request.user
):
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_400_BAD_REQUEST,
)
issue = InboxIssueDetailSerializer(inbox_issue).data
issue = IntakeIssueDetailSerializer(intake_issue).data
return Response(
issue,
status=status.HTTP_200_OK,
@@ -630,23 +620,23 @@ class InboxIssueViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
def destroy(self, request, slug, project_id, pk):
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
inbox_issue = InboxIssue.objects.get(
intake_issue = IntakeIssue.objects.get(
issue_id=pk,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox_id,
intake_id=intake_id,
)
# Check the issue status
if inbox_issue.status in [-2, -1, 0, 2]:
if intake_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=pk
).first()
issue.delete()
inbox_issue.delete()
intake_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+8 -8
View File
@@ -171,10 +171,10 @@ class IssueArchiveViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -200,10 +200,10 @@ class IssueArchiveViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
+8 -8
View File
@@ -336,10 +336,10 @@ class IssueViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -364,10 +364,10 @@ class IssueViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
+8 -8
View File
@@ -160,10 +160,10 @@ class ModuleIssueViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -189,10 +189,10 @@ class ModuleIssueViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -53,9 +53,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
mentioned = request.GET.get("mentioned", False)
q_filters = Q()
inbox_issue = Issue.objects.filter(
intake_issue = Issue.objects.filter(
pk=OuterRef("entity_identifier"),
issue_inbox__status__in=[0, 2, -2],
issue_intake__status__in=[0, 2, -2],
workspace__slug=self.kwargs.get("slug"),
)
@@ -64,7 +64,8 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
workspace__slug=slug, receiver_id=request.user.id
)
.filter(entity_name="issue")
.annotate(is_inbox_issue=Exists(inbox_issue))
.annotate(is_inbox_issue=Exists(intake_issue))
.annotate(is_intake_issue=Exists(intake_issue))
.annotate(
is_mentioned_notification=Case(
When(sender__icontains="mentioned", then=True),
+9 -9
View File
@@ -38,7 +38,7 @@ from plane.app.permissions import (
from plane.db.models import (
UserFavorite,
Cycle,
Inbox,
Intake,
DeployBoard,
IssueUserProperty,
Issue,
@@ -449,14 +449,14 @@ class ProjectViewSet(BaseViewSet):
if serializer.is_valid():
serializer.save()
if serializer.data["inbox_view"]:
inbox = Inbox.objects.filter(
if serializer.data["intake_view"] or request.data.get("inbox_view", False):
intake = Intake.objects.filter(
project=project,
is_default=True,
).first()
if not inbox:
Inbox.objects.create(
name=f"{project.name} Inbox",
if not intake:
Intake.objects.create(
name=f"{project.name} Intake",
project=project,
is_default=True,
)
@@ -465,7 +465,7 @@ class ProjectViewSet(BaseViewSet):
State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Inbox Issues",
description="Default state for managing all Intake Issues",
project_id=pk,
color="#ff7700",
is_triage=True,
@@ -759,7 +759,7 @@ class DeployBoardViewSet(BaseViewSet):
def create(self, request, slug, project_id):
comments = request.data.get("is_comments_enabled", False)
reactions = request.data.get("is_reactions_enabled", False)
inbox = request.data.get("inbox", None)
intake = request.data.get("intake", None)
votes = request.data.get("is_votes_enabled", False)
views = request.data.get(
"views",
@@ -777,7 +777,7 @@ class DeployBoardViewSet(BaseViewSet):
entity_identifier=project_id,
project_id=project_id,
)
project_deploy_board.inbox = inbox
project_deploy_board.intake = intake
project_deploy_board.view_props = views
project_deploy_board.is_votes_enabled = votes
project_deploy_board.is_comments_enabled = comments
@@ -21,7 +21,6 @@ from plane.db.models import (
Project,
ProjectMember,
Workspace,
TeamMember,
IssueUserProperty,
WorkspaceMember,
)
@@ -342,54 +341,6 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class AddTeamToProjectEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
def post(self, request, slug, project_id):
team_members = TeamMember.objects.filter(
workspace__slug=slug, team__in=request.data.get("teams", [])
).values_list("member", flat=True)
if len(team_members) == 0:
return Response(
{"error": "No such team exists"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
project_members = []
issue_props = []
for member in team_members:
project_members.append(
ProjectMember(
project_id=project_id,
member_id=member,
workspace=workspace,
created_by=request.user,
)
)
issue_props.append(
IssueUserProperty(
project_id=project_id,
user_id=member,
workspace=workspace,
created_by=request.user,
)
)
ProjectMember.objects.bulk_create(
project_members, batch_size=10, ignore_conflicts=True
)
_ = IssueUserProperty.objects.bulk_create(
issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class ProjectMemberUserEndpoint(BaseAPIView):
+8 -8
View File
@@ -370,10 +370,10 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -399,10 +399,10 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
+1 -62
View File
@@ -24,7 +24,6 @@ from plane.app.permissions import (
# Module imports
from plane.app.serializers import (
ProjectMemberRoleSerializer,
TeamSerializer,
UserLiteSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
@@ -34,7 +33,6 @@ from plane.app.views.base import BaseAPIView
from plane.db.models import (
Project,
ProjectMember,
Team,
User,
Workspace,
WorkspaceMember,
@@ -351,63 +349,4 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView):
project_members_dict[str(project_id)] = []
project_members_dict[str(project_id)].append(project_member)
return Response(project_members_dict, status=status.HTTP_200_OK)
class TeamMemberViewSet(BaseViewSet):
serializer_class = TeamSerializer
model = Team
permission_classes = [
WorkSpaceAdminPermission,
]
search_fields = [
"member__display_name",
"member__first_name",
]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "workspace__owner")
.prefetch_related("members")
)
def create(self, request, slug):
members = list(
WorkspaceMember.objects.filter(
workspace__slug=slug,
member__id__in=request.data.get("members", []),
is_active=True,
)
.annotate(member_str_id=Cast("member", output_field=CharField()))
.distinct()
.values_list("member_str_id", flat=True)
)
if len(members) != len(request.data.get("members", [])):
users = list(
set(request.data.get("members", [])).difference(members)
)
users = User.objects.filter(pk__in=users)
serializer = UserLiteSerializer(users, many=True)
return Response(
{
"error": f"{len(users)} of the member(s) are not a part of the workspace",
"members": serializer.data,
},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
serializer = TeamSerializer(
data=request.data, context={"workspace": workspace}
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(project_members_dict, status=status.HTTP_200_OK)
+8 -8
View File
@@ -204,10 +204,10 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -231,10 +231,10 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
+14 -14
View File
@@ -30,8 +30,8 @@ from plane.db.models import (
Page,
ProjectPage,
PageLabel,
Inbox,
InboxIssue,
Intake,
IntakeIssue,
)
@@ -47,7 +47,7 @@ def create_project(workspace, user_id):
: random.randint(2, 12 if len(name) - 1 >= 12 else len(name) - 1)
].upper(),
created_by_id=user_id,
inbox_view=True,
intake_view=True,
)
# Add current member as project member
@@ -406,18 +406,18 @@ def create_issues(workspace, project, user_id, issue_count):
return issues
def create_inbox_issues(workspace, project, user_id, inbox_issue_count):
issues = create_issues(workspace, project, user_id, inbox_issue_count)
inbox, create = Inbox.objects.get_or_create(
name="Inbox",
def create_intake_issues(workspace, project, user_id, intake_issue_count):
issues = create_issues(workspace, project, user_id, intake_issue_count)
intake, create = Intake.objects.get_or_create(
name="Intake",
project=project,
is_default=True,
)
InboxIssue.objects.bulk_create(
IntakeIssue.objects.bulk_create(
[
InboxIssue(
IntakeIssue(
issue=issue,
inbox=inbox,
intake=intake,
status=(status := [-2, -1, 0, 1, 2][random.randint(0, 4)]),
snoozed_till=(
datetime.now() + timedelta(days=random.randint(1, 30))
@@ -599,7 +599,7 @@ def create_dummy_data(
cycle_count,
module_count,
pages_count,
inbox_issue_count,
intake_issue_count,
):
workspace = Workspace.objects.get(slug=slug)
@@ -660,12 +660,12 @@ def create_dummy_data(
issue_count=issue_count,
)
# create inbox issues
create_inbox_issues(
# create intake issues
create_intake_issues(
workspace=workspace,
project=project,
user_id=user_id,
inbox_issue_count=inbox_issue_count,
intake_issue_count=intake_issue_count,
)
# create issue parent
@@ -1567,7 +1567,7 @@ def delete_draft_issue_activity(
)
def create_inbox_activity(
def create_intake_activity(
requested_data,
current_instance,
issue_id,
@@ -1596,8 +1596,8 @@ def create_inbox_activity(
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
comment="updated the inbox status",
field="inbox",
comment="updated the intake status",
field="intake",
verb=requested_data.get("status"),
actor_id=actor_id,
epoch=epoch,
@@ -1620,7 +1620,7 @@ def issue_activity(
subscriber=True,
notification=False,
origin=None,
inbox=None,
intake=None,
):
try:
issue_activities = []
@@ -1668,7 +1668,7 @@ def issue_activity(
"issue_draft.activity.created": create_draft_issue_activity,
"issue_draft.activity.updated": update_draft_issue_activity,
"issue_draft.activity.deleted": delete_draft_issue_activity,
"inbox.activity.created": create_inbox_activity,
"intake.activity.created": create_intake_activity,
}
func = ACTIVITY_MAPPER.get(type)
@@ -1695,12 +1695,12 @@ def issue_activity(
event=(
"issue_comment"
if activity.field == "comment"
else "inbox_issue" if inbox else "issue"
else "intake_issue" if intake else "issue"
),
event_id=(
activity.issue_comment_id
if activity.field == "comment"
else inbox if inbox else activity.issue_id
else intake if intake else activity.issue_id
),
verb=activity.verb,
field=(
@@ -51,10 +51,10 @@ def archive_old_issues():
& Q(issue_module__isnull=False)
),
).filter(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True)
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True)
)
# Check if Issues
@@ -129,10 +129,10 @@ def close_old_issues():
& Q(issue_module__isnull=False)
),
).filter(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True)
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True)
)
# Check if Issues
+4 -4
View File
@@ -27,7 +27,7 @@ from plane.api.serializers import (
ModuleSerializer,
ProjectSerializer,
UserLiteSerializer,
InboxIssueSerializer,
IntakeIssueSerializer,
)
from plane.db.models import (
Cycle,
@@ -40,7 +40,7 @@ from plane.db.models import (
User,
Webhook,
WebhookLog,
InboxIssue,
IntakeIssue,
)
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@@ -54,7 +54,7 @@ SERIALIZER_MAPPER = {
"module_issue": ModuleIssueSerializer,
"issue_comment": IssueCommentSerializer,
"user": UserLiteSerializer,
"inbox_issue": InboxIssueSerializer,
"intake_issue": IntakeIssueSerializer,
}
MODEL_MAPPER = {
@@ -66,7 +66,7 @@ MODEL_MAPPER = {
"module_issue": ModuleIssue,
"issue_comment": IssueComment,
"user": User,
"inbox_issue": InboxIssue,
"intake_issue": IntakeIssue,
}
@@ -62,13 +62,15 @@ class Command(BaseCommand):
project_count = int(input("Number of projects to be created: "))
for i in range(project_count):
print(f"Please provide the following details for project {i+1}:")
print(
f"Please provide the following details for project {i+1}:"
)
issue_count = int(input("Number of issues to be created: "))
cycle_count = int(input("Number of cycles to be created: "))
module_count = int(input("Number of modules to be created: "))
pages_count = int(input("Number of pages to be created: "))
inbox_issue_count = int(
input("Number of inbox issues to be created: ")
intake_issue_count = int(
input("Number of intake issues to be created: ")
)
from plane.bgtasks.dummy_data_task import create_dummy_data
@@ -81,7 +83,7 @@ class Command(BaseCommand):
cycle_count=cycle_count,
module_count=module_count,
pages_count=pages_count,
inbox_issue_count=inbox_issue_count,
intake_issue_count=intake_issue_count,
)
self.stdout.write(
@@ -0,0 +1,74 @@
# Generated by Django 4.2.15 on 2024-11-08 13:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('db', '0084_remove_label_label_unique_name_project_when_deleted_at_null_and_more'),
]
operations = [
migrations.AlterUniqueTogether(
name='teammember',
unique_together=None,
),
migrations.RemoveField(
model_name='teammember',
name='created_by',
),
migrations.RemoveField(
model_name='teammember',
name='member',
),
migrations.RemoveField(
model_name='teammember',
name='team',
),
migrations.RemoveField(
model_name='teammember',
name='updated_by',
),
migrations.RemoveField(
model_name='teammember',
name='workspace',
),
migrations.AlterUniqueTogether(
name='teampage',
unique_together=None,
),
migrations.RemoveField(
model_name='teampage',
name='created_by',
),
migrations.RemoveField(
model_name='teampage',
name='page',
),
migrations.RemoveField(
model_name='teampage',
name='team',
),
migrations.RemoveField(
model_name='teampage',
name='updated_by',
),
migrations.RemoveField(
model_name='teampage',
name='workspace',
),
migrations.RemoveField(
model_name='page',
name='teams',
),
migrations.DeleteModel(
name='Team',
),
migrations.DeleteModel(
name='TeamMember',
),
migrations.DeleteModel(
name='TeamPage',
),
]
@@ -0,0 +1,141 @@
# Generated by Django 4.2.15 on 2024-11-06 08:41
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0084_remove_label_label_unique_name_project_when_deleted_at_null_and_more'),
]
operations = [
migrations.RenameModel(
old_name="Inbox",
new_name="Intake",
),
migrations.AlterModelTable(
name="Intake",
table="intakes",
),
migrations.AlterModelOptions(
name="Intake",
options={
"verbose_name": "Intake",
"verbose_name_plural": "Intakes",
"ordering": ("name",),
},
),
migrations.AlterField(
model_name="Intake",
name="description",
field=models.TextField(
blank=True, verbose_name="Intake Description"
),
),
migrations.RenameModel(
old_name="InboxIssue",
new_name="IntakeIssue",
),
# Rename the 'inbox' field to 'intake'
migrations.RenameField(
model_name="IntakeIssue",
old_name="inbox",
new_name="intake",
),
# Update ForeignKey related_name for 'intake'
migrations.AlterField(
model_name="IntakeIssue",
name="intake",
field=models.ForeignKey(
"db.Intake",
related_name="issue_intake",
on_delete=django.db.models.deletion.CASCADE,
),
),
# Update ForeignKey related_name for 'issue'
migrations.AlterField(
model_name="IntakeIssue",
name="issue",
field=models.ForeignKey(
"db.Issue",
related_name="issue_intake",
on_delete=django.db.models.deletion.CASCADE,
),
),
# Update ForeignKey related_name for 'duplicate_to'
migrations.AlterField(
model_name="IntakeIssue",
name="duplicate_to",
field=models.ForeignKey(
"db.Issue",
related_name="intake_duplicate",
on_delete=django.db.models.deletion.SET_NULL,
null=True,
),
),
# Update Meta options
migrations.AlterModelOptions(
name="IntakeIssue",
options={
"verbose_name": "IntakeIssue",
"verbose_name_plural": "IntakeIssues",
"ordering": ("-created_at",),
},
),
# Update db_table
migrations.AlterModelTable(
name="IntakeIssue",
table="intake_issues",
),
migrations.RenameField(
model_name="project",
old_name="inbox_view",
new_name="intake_view",
),
migrations.RenameField(
model_name="deployboard",
old_name="inbox",
new_name="intake",
),
migrations.RenameField(
model_name="projectdeployboard",
old_name="inbox",
new_name="intake",
),
migrations.RemoveConstraint(
model_name="intake",
name="inbox_unique_name_project_when_deleted_at_null",
),
migrations.AlterField(
model_name="deployboard",
name="intake",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="publish_intake",
to="db.intake",
),
),
migrations.AlterField(
model_name="projectdeployboard",
name="intake",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="board_intake",
to="db.intake",
),
),
migrations.AddConstraint(
model_name="intake",
constraint=models.UniqueConstraint(
condition=models.Q(("deleted_at__isnull", True)),
fields=("name", "project"),
name="intake_unique_name_project_when_deleted_at_null",
),
),
]
+2 -4
View File
@@ -15,7 +15,7 @@ from .draft import (
from .estimate import Estimate, EstimatePoint
from .exporter import ExporterHistory
from .importer import Importer
from .inbox import Inbox, InboxIssue
from .intake import Intake, IntakeIssue
from .integration import (
GithubCommentSync,
GithubIssueSync,
@@ -77,8 +77,6 @@ from .user import Account, Profile, User
from .view import IssueView
from .webhook import Webhook, WebhookLog
from .workspace import (
Team,
TeamMember,
Workspace,
WorkspaceBaseModel,
WorkspaceMember,
@@ -93,7 +91,7 @@ from .page import Page, PageLog, PageLabel
from .estimate import Estimate, EstimatePoint
from .inbox import Inbox, InboxIssue
from .intake import Intake, IntakeIssue
from .analytic import AnalyticView
+3 -3
View File
@@ -34,9 +34,9 @@ class DeployBoard(WorkspaceBaseModel):
)
is_comments_enabled = models.BooleanField(default=False)
is_reactions_enabled = models.BooleanField(default=False)
inbox = models.ForeignKey(
"db.Inbox",
related_name="board_inbox",
intake = models.ForeignKey(
"db.Intake",
related_name="publish_intake",
on_delete=models.SET_NULL,
null=True,
)
@@ -5,17 +5,17 @@ from django.db import models
from plane.db.models.project import ProjectBaseModel
class Inbox(ProjectBaseModel):
class Intake(ProjectBaseModel):
name = models.CharField(max_length=255)
description = models.TextField(
verbose_name="Inbox Description", blank=True
verbose_name="Intake Description", blank=True
)
is_default = models.BooleanField(default=False)
view_props = models.JSONField(default=dict)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the Inbox"""
"""Return name of the intake"""
return f"{self.name} <{self.project.name}>"
class Meta:
@@ -24,21 +24,21 @@ class Inbox(ProjectBaseModel):
models.UniqueConstraint(
fields=["name", "project"],
condition=models.Q(deleted_at__isnull=True),
name="inbox_unique_name_project_when_deleted_at_null",
name="intake_unique_name_project_when_deleted_at_null",
)
]
verbose_name = "Inbox"
verbose_name_plural = "Inboxes"
db_table = "inboxes"
verbose_name = "Intake"
verbose_name_plural = "Intakes"
db_table = "intakes"
ordering = ("name",)
class InboxIssue(ProjectBaseModel):
inbox = models.ForeignKey(
"db.Inbox", related_name="issue_inbox", on_delete=models.CASCADE
class IntakeIssue(ProjectBaseModel):
intake = models.ForeignKey(
"db.Intake", related_name="issue_intake", on_delete=models.CASCADE
)
issue = models.ForeignKey(
"db.Issue", related_name="issue_inbox", on_delete=models.CASCADE
"db.Issue", related_name="issue_intake", on_delete=models.CASCADE
)
status = models.IntegerField(
choices=(
@@ -53,7 +53,7 @@ class InboxIssue(ProjectBaseModel):
snoozed_till = models.DateTimeField(null=True)
duplicate_to = models.ForeignKey(
"db.Issue",
related_name="inbox_duplicate",
related_name="intake_duplicate",
on_delete=models.SET_NULL,
null=True,
)
@@ -69,11 +69,11 @@ class InboxIssue(ProjectBaseModel):
extra = models.JSONField(default=dict)
class Meta:
verbose_name = "InboxIssue"
verbose_name_plural = "InboxIssues"
db_table = "inbox_issues"
verbose_name = "IntakeIssue"
verbose_name_plural = "IntakeIssues"
db_table = "intake_issues"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the Issue"""
return f"{self.issue.name} <{self.inbox.name}>"
return f"{self.issue.name} <{self.intake.name}>"
+4 -4
View File
@@ -86,10 +86,10 @@ class IssueManager(SoftDeletionManager):
super()
.get_queryset()
.filter(
models.Q(issue_inbox__status=1)
| models.Q(issue_inbox__status=-1)
| models.Q(issue_inbox__status=2)
| models.Q(issue_inbox__isnull=True)
models.Q(issue_intake__status=1)
| models.Q(issue_intake__status=-1)
| models.Q(issue_intake__status=2)
| models.Q(issue_intake__isnull=True)
)
.filter(deleted_at__isnull=True)
.filter(state__is_triage=False)
-30
View File
@@ -52,9 +52,6 @@ class Page(BaseModel):
projects = models.ManyToManyField(
"db.Project", related_name="pages", through="db.ProjectPage"
)
teams = models.ManyToManyField(
"db.Team", related_name="pages", through="db.TeamPage"
)
class Meta:
verbose_name = "Page"
@@ -169,33 +166,6 @@ class ProjectPage(BaseModel):
def __str__(self):
return f"{self.project.name} {self.page.name}"
class TeamPage(BaseModel):
team = models.ForeignKey(
"db.Team", on_delete=models.CASCADE, related_name="team_pages"
)
page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, related_name="team_pages"
)
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="team_pages"
)
class Meta:
unique_together = ["team", "page", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["team", "page"],
condition=models.Q(deleted_at__isnull=True),
name="team_page_unique_team_page_when_deleted_at_null",
)
]
verbose_name = "Team Page"
verbose_name_plural = "Team Pages"
db_table = "team_pages"
ordering = ("-created_at",)
class PageVersion(BaseModel):
workspace = models.ForeignKey(
"db.Workspace",
+4 -4
View File
@@ -95,7 +95,7 @@ class Project(BaseModel):
cycle_view = models.BooleanField(default=True)
issue_views_view = models.BooleanField(default=True)
page_view = models.BooleanField(default=True)
inbox_view = models.BooleanField(default=False)
intake_view = models.BooleanField(default=False)
is_time_tracking_enabled = models.BooleanField(default=False)
is_issue_type_enabled = models.BooleanField(default=False)
guest_view_all_features = models.BooleanField(default=False)
@@ -309,9 +309,9 @@ class ProjectDeployBoard(ProjectBaseModel):
)
comments = models.BooleanField(default=False)
reactions = models.BooleanField(default=False)
inbox = models.ForeignKey(
"db.Inbox",
related_name="bord_inbox",
intake = models.ForeignKey(
"db.Intake",
related_name="board_intake",
on_delete=models.SET_NULL,
null=True,
)
-65
View File
@@ -259,71 +259,6 @@ class WorkspaceMemberInvite(BaseModel):
return f"{self.workspace.name} {self.email} {self.accepted}"
class Team(BaseModel):
name = models.CharField(max_length=255, verbose_name="Team Name")
description = models.TextField(verbose_name="Team Description", blank=True)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="members",
through="TeamMember",
through_fields=("team", "member"),
)
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="workspace_team"
)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the team"""
return f"{self.name} <{self.workspace.name}>"
class Meta:
unique_together = ["name", "workspace", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["name", "workspace"],
condition=models.Q(deleted_at__isnull=True),
name="team_unique_name_workspace_when_deleted_at_null",
)
]
verbose_name = "Team"
verbose_name_plural = "Teams"
db_table = "teams"
ordering = ("-created_at",)
class TeamMember(BaseModel):
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="team_member"
)
team = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name="team_member"
)
member = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="team_member",
)
def __str__(self):
return self.team.name
class Meta:
unique_together = ["team", "member", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["team", "member"],
condition=models.Q(deleted_at__isnull=True),
name="team_member_unique_team_member_when_deleted_at_null",
)
]
verbose_name = "Team Member"
verbose_name_plural = "Team Members"
db_table = "team_members"
ordering = ("-created_at",)
class WorkspaceTheme(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="themes"
@@ -9,16 +9,16 @@ from .project import ProjectLiteSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from plane.db.models import (
Issue,
InboxIssue,
IntakeIssue,
)
class InboxIssueSerializer(BaseSerializer):
class IntakeIssueSerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(source="issue", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = InboxIssue
model = IntakeIssue
fields = "__all__"
read_only_fields = [
"project",
@@ -26,14 +26,14 @@ class InboxIssueSerializer(BaseSerializer):
]
class InboxIssueLiteSerializer(BaseSerializer):
class IntakeIssueLiteSerializer(BaseSerializer):
class Meta:
model = InboxIssue
model = IntakeIssue
fields = ["id", "status", "duplicate_to", "snoozed_till", "source"]
read_only_fields = fields
class IssueStateInboxSerializer(BaseSerializer):
class IssueStateIntakeSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
label_details = LabelLiteSerializer(
@@ -44,7 +44,7 @@ class IssueStateInboxSerializer(BaseSerializer):
)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
issue_intake = IntakeIssueLiteSerializer(read_only=True, many=True)
class Meta:
model = Issue
+2 -2
View File
@@ -1,11 +1,11 @@
from .inbox import urlpatterns as inbox_urls
from .intake import urlpatterns as intake_urls
from .issue import urlpatterns as issue_urls
from .project import urlpatterns as project_urls
from .asset import urlpatterns as asset_urls
urlpatterns = [
*inbox_urls,
*intake_urls,
*issue_urls,
*project_urls,
*asset_urls,
@@ -2,15 +2,26 @@ from django.urls import path
from plane.space.views import (
InboxIssuePublicViewSet,
IntakeIssuePublicViewSet,
IssueVotePublicViewSet,
WorkspaceProjectDeployBoardEndpoint,
)
urlpatterns = [
path(
"anchor/<str:anchor>/inboxes/<uuid:inbox_id>/inbox-issues/",
InboxIssuePublicViewSet.as_view(
"anchor/<str:anchor>/intakes/<uuid:intake_id>/intake-issues/",
IntakeIssuePublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="intake-issue",
),
path(
"anchor/<str:anchor>/intakes/<uuid:intake_id>/inbox-issues/",
IntakeIssuePublicViewSet.as_view(
{
"get": "list",
"post": "create",
@@ -19,15 +30,15 @@ urlpatterns = [
name="inbox-issue",
),
path(
"anchor/<str:anchor>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
InboxIssuePublicViewSet.as_view(
"anchor/<str:anchor>/intakes/<uuid:intake_id>/intake-issues/<uuid:pk>/",
IntakeIssuePublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
name="intake-issue",
),
path(
"workspaces/<str:slug>/project-boards/",
+1 -1
View File
@@ -14,7 +14,7 @@ from .issue import (
ProjectIssuesPublicEndpoint,
)
from .inbox import InboxIssuePublicViewSet
from .intake import IntakeIssuePublicViewSet
from .cycle import ProjectCyclesEndpoint
@@ -13,7 +13,7 @@ from rest_framework.response import Response
# Module imports
from .base import BaseViewSet
from plane.db.models import (
InboxIssue,
IntakeIssue,
Issue,
State,
IssueLink,
@@ -22,17 +22,17 @@ from plane.db.models import (
)
from plane.app.serializers import (
IssueSerializer,
InboxIssueSerializer,
IntakeIssueSerializer,
IssueCreateSerializer,
IssueStateInboxSerializer,
IssueStateIntakeSerializer,
)
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activities_task import issue_activity
class InboxIssuePublicViewSet(BaseViewSet):
serializer_class = InboxIssueSerializer
model = InboxIssue
class IntakeIssuePublicViewSet(BaseViewSet):
serializer_class = IntakeIssueSerializer
model = IntakeIssue
filterset_fields = [
"status",
@@ -52,34 +52,34 @@ class InboxIssuePublicViewSet(BaseViewSet):
| Q(snoozed_till__isnull=True),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
inbox_id=self.kwargs.get("inbox_id"),
intake_id=self.kwargs.get("intake_id"),
)
.select_related("issue", "workspace", "project")
)
return InboxIssue.objects.none()
return IntakeIssue.objects.none()
def list(self, request, anchor, inbox_id):
def list(self, request, anchor, intake_id):
project_deploy_board = DeployBoard.objects.get(
anchor=anchor, entity_name="project"
)
if project_deploy_board.inbox is None:
if project_deploy_board.intake is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
{"error": "Intake is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.objects.filter(
issue_inbox__inbox_id=inbox_id,
issue_intake__intake_id=intake_id,
workspace_id=project_deploy_board.workspace_id,
project_id=project_deploy_board.project_id,
)
.filter(**filters)
.annotate(bridge_id=F("issue_inbox__id"))
.annotate(bridge_id=F("issue_intake__id"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
.order_by("issue_intake__snoozed_till", "issue_intake__status")
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
@@ -105,26 +105,26 @@ class InboxIssuePublicViewSet(BaseViewSet):
)
.prefetch_related(
Prefetch(
"issue_inbox",
queryset=InboxIssue.objects.only(
"issue_intake",
queryset=IntakeIssue.objects.only(
"status", "duplicate_to", "snoozed_till", "source"
),
)
)
)
issues_data = IssueStateInboxSerializer(issues, many=True).data
issues_data = IssueStateIntakeSerializer(issues, many=True).data
return Response(
issues_data,
status=status.HTTP_200_OK,
)
def create(self, request, anchor, inbox_id):
def create(self, request, anchor, intake_id):
project_deploy_board = DeployBoard.objects.get(
anchor=anchor, entity_name="project"
)
if project_deploy_board.inbox is None:
if project_deploy_board.intake is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
{"error": "Intake is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -151,7 +151,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
state, _ = State.objects.get_or_create(
name="Triage",
group="backlog",
description="Default state for managing all Inbox Issues",
description="Default state for managing all Intake Issues",
project_id=project_deploy_board.project_id,
color="#ff7700",
)
@@ -178,37 +178,37 @@ class InboxIssuePublicViewSet(BaseViewSet):
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
# create an inbox issue
InboxIssue.objects.create(
inbox_id=inbox_id,
# create an intake issue
IntakeIssue.objects.create(
intake_id=intake_id,
project_id=project_deploy_board.project_id,
issue=issue,
source=request.data.get("source", "in-app"),
source=request.data.get("source", "IN-APP"),
)
serializer = IssueStateInboxSerializer(issue)
serializer = IssueStateIntakeSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, anchor, inbox_id, pk):
def partial_update(self, request, anchor, intake_id, pk):
project_deploy_board = DeployBoard.objects.get(
anchor=anchor, entity_name="project"
)
if project_deploy_board.inbox is None:
if project_deploy_board.intake is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
{"error": "Intake is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
inbox_issue = InboxIssue.objects.get(
intake_issue = IntakeIssue.objects.get(
pk=pk,
workspace_id=project_deploy_board.workspace_id,
project_id=project_deploy_board.project_id,
inbox_id=inbox_id,
intake_id=intake_id,
)
# Get the project member
if str(inbox_issue.created_by_id) != str(request.user.id):
if str(intake_issue.created_by_id) != str(request.user.id):
return Response(
{"error": "You cannot edit inbox issues"},
{"error": "You cannot edit intake issues"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -216,7 +216,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
issue_data = request.data.pop("issue", False)
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
pk=intake_issue.issue_id,
workspace_id=project_deploy_board.workspace_id,
project_id=project_deploy_board.project_id,
)
@@ -256,52 +256,52 @@ class InboxIssuePublicViewSet(BaseViewSet):
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
def retrieve(self, request, anchor, inbox_id, pk):
def retrieve(self, request, anchor, intake_id, pk):
project_deploy_board = DeployBoard.objects.get(
anchor=anchor, entity_name="project"
)
if project_deploy_board.inbox is None:
if project_deploy_board.intake is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
{"error": "Intake is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
inbox_issue = InboxIssue.objects.get(
intake_issue = IntakeIssue.objects.get(
pk=pk,
workspace_id=project_deploy_board.workspace_id,
project_id=project_deploy_board.project_id,
inbox_id=inbox_id,
intake_id=intake_id,
)
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
pk=intake_issue.issue_id,
workspace_id=project_deploy_board.workspace_id,
project_id=project_deploy_board.project_id,
)
serializer = IssueStateInboxSerializer(issue)
serializer = IssueStateIntakeSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, anchor, inbox_id, pk):
def destroy(self, request, anchor, intake_id, pk):
project_deploy_board = DeployBoard.objects.get(
anchor=anchor, entity_name="project"
)
if project_deploy_board.inbox is None:
if project_deploy_board.intake is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
{"error": "Intake is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
inbox_issue = InboxIssue.objects.get(
intake_issue = IntakeIssue.objects.get(
pk=pk,
workspace_id=project_deploy_board.workspace_id,
project_id=project_deploy_board.project_id,
inbox_id=inbox_id,
intake_id=intake_id,
)
if str(inbox_issue.created_by_id) != str(request.user.id):
if str(intake_issue.created_by_id) != str(request.user.id):
return Response(
{"error": "You cannot delete inbox issue"},
{"error": "You cannot delete intake issue"},
status=status.HTTP_400_BAD_REQUEST,
)
inbox_issue.delete()
intake_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+8 -8
View File
@@ -194,10 +194,10 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__status=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -222,10 +222,10 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__status=True),
archived_at__isnull=True,
is_draft=False,
),
+24 -2
View File
@@ -451,6 +451,27 @@ def filter_module(params, issue_filter, method, prefix=""):
return issue_filter
def filter_intake_status(params, issue_filter, method, prefix=""):
if method == "GET":
status = [
item
for item in params.get("intake_status").split(",")
if item != "null"
]
if len(status) and "" not in status:
issue_filter[f"{prefix}issue_intake__status__in"] = status
else:
if (
params.get("intake_status", None)
and len(params.get("intake_status"))
and params.get("intake_status") != "null"
):
issue_filter[f"{prefix}issue_intake__status__in"] = params.get(
"inbox_status"
)
return issue_filter
def filter_inbox_status(params, issue_filter, method, prefix=""):
if method == "GET":
status = [
@@ -459,14 +480,14 @@ def filter_inbox_status(params, issue_filter, method, prefix=""):
if item != "null"
]
if len(status) and "" not in status:
issue_filter[f"{prefix}issue_inbox__status__in"] = status
issue_filter[f"{prefix}issue_intake__status__in"] = status
else:
if (
params.get("inbox_status", None)
and len(params.get("inbox_status"))
and params.get("inbox_status") != "null"
):
issue_filter[f"{prefix}issue_inbox__status__in"] = params.get(
issue_filter[f"{prefix}issue_intake__status__in"] = params.get(
"inbox_status"
)
return issue_filter
@@ -562,6 +583,7 @@ def issue_filters(query_params, method, prefix=""):
"project": filter_project,
"cycle": filter_cycle,
"module": filter_module,
"intake_status": filter_intake_status,
"inbox_status": filter_inbox_status,
"sub_issue": filter_sub_issue_toggle,
"subscriber": filter_subscribed_issues,
@@ -55,7 +55,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
},
onSynced: () => setHasServerSynced(true),
}),
[id, realtimeConfig, serverHandler, user.id]
[id, realtimeConfig, serverHandler, user]
);
// destroy and disconnect connection on unmount
+1 -1
View File
@@ -253,7 +253,7 @@ export class IssueDetailStore implements IIssueDetailStore {
anchor,
{
entity_identifier: commentID ?? "",
entity_type: EFileAssetType.ISSUE_ATTACHMENT,
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
},
file
);
+1 -1
View File
@@ -13,7 +13,7 @@ export const getRelationActivityContent = (activity: TIssueActivity | undefined)
case "duplicate":
return activity.old_value === "" ? `marked this issue as duplicate of ` : `removed this issue as a duplicate of `;
case "relates_to":
activity.old_value === "" ? `marked that this issue relates to ` : `removed the relation from `;
return activity.old_value === "" ? `marked that this issue relates to ` : `removed the relation from `;
}
return;
+7 -4
View File
@@ -2,21 +2,24 @@
// icons
import { Home, Inbox, PenSquare } from "lucide-react";
// ui
import { UserActivityIcon } from "@plane/ui";
import { Props } from "@/components/icons/types";
import { TLinkOptions } from "@/constants/dashboard";
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
// plane web types
import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard";
export const SIDEBAR_USER_MENU_ITEMS: {
key: string;
export type TSidebarUserMenuItems = {
key: TSidebarUserMenuItemKeys;
label: string;
href: string;
access: EUserPermissions[];
highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) => boolean;
Icon: React.FC<Props>;
}[] = [
};
export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [
{
key: "home",
label: "Home",
+5
View File
@@ -0,0 +1,5 @@
// plane web types
import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const isUserFeatureEnabled = (featureKey: TSidebarUserMenuItemKeys) => true;
+1
View File
@@ -0,0 +1 @@
export type TSidebarUserMenuItemKeys = "home" | "your-work" | "notifications" | "drafts";
@@ -54,7 +54,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
suggestions: mentionSuggestions,
}}
{...rest}
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
containerClassName={cn("relative pl-3", containerClassName)}
/>
);
});
@@ -40,13 +40,13 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
<>
<div
key={linkId}
className="col-span-12 lg:col-span-6 xl:col-span-4 2xl:col-span-3 3xl:col-span-2 flex items-center justify-between gap-3 h-8 flex-shrink-0 px-3 bg-custom-background-90 border-[0.5px] border-custom-border-200 rounded"
className="group col-span-12 lg:col-span-6 xl:col-span-4 2xl:col-span-3 3xl:col-span-2 flex items-center justify-between gap-3 h-10 flex-shrink-0 px-3 bg-custom-background-90 hover:bg-custom-background-80 border-[0.5px] border-custom-border-200 rounded"
>
<div className="flex items-center gap-2.5 truncate">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
<div className="flex items-center gap-2.5 truncate flex-grow">
<LinkIcon className="size-4 flex-shrink-0 text-custom-text-400 group-hover:text-custom-text-200" />
<Tooltip tooltipContent={linkDetail.url} isMobile={isMobile}>
<span
className="truncate text-xs cursor-pointer"
className="truncate text-sm cursor-pointer flex-grow"
onClick={() => {
copyTextToClipboard(linkDetail.url);
setToast({
@@ -60,21 +60,21 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
</span>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<p className="p-1 text-xs align-bottom leading-5 text-custom-text-300">
<div className="flex items-center gap-1 flex-shrink-0">
<p className="p-1 text-xs align-bottom leading-5 text-custom-text-400 group-hover-text-custom-text-200">
{calculateTimeAgoShort(linkDetail.created_at)}
</p>
<a
href={linkDetail.url}
target="_blank"
rel="noopener noreferrer"
className="relative grid place-items-center rounded p-1 text-custom-text-300 outline-none hover:text-custom-text-200 cursor-pointer hover:bg-custom-background-80"
className="relative grid place-items-center rounded p-1 text-custom-text-400 outline-none group-hover:text-custom-text-200 cursor-pointer hover:bg-custom-background-80"
>
<ExternalLink className="h-3.5 w-3.5 stroke-[1.5]" />
</a>
<CustomMenu
ellipsis
buttonClassName="text-custom-text-300 hover:text-custom-text-200"
buttonClassName="text-custom-text-400 group-hover:text-custom-text-200"
placement="bottom-end"
closeOnSelect
disabled={isNotAllowed}
@@ -27,7 +27,7 @@ export const LinkList: FC<TLinkList> = observer((props) => {
if (!issueLinks) return null;
return (
<div className="grid grid-cols-12 3xl:grid-cols-10 gap-2 px-9 py-4">
<div className="flex flex-col gap-2 py-4">
{issueLinks.map((linkId) => (
<IssueLinkItem key={linkId} linkId={linkId} linkOperations={linkOperations} isNotAllowed={disabled} />
))}
@@ -149,6 +149,15 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
}
}, [projectId, workspaceSlug]);
const userConfig = useMemo(
() => ({
id: currentUser?.id ?? "",
name: currentUser?.display_name ?? "",
color: generateRandomColor(currentUser?.id ?? ""),
}),
[currentUser]
);
if (pageId === undefined || !realtimeConfig) return <PageContentLoader />;
return (
@@ -214,11 +223,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
}}
realtimeConfig={realtimeConfig}
serverHandler={serverHandler}
user={{
id: currentUser?.id ?? "",
name: currentUser?.display_name ?? "",
color: generateRandomColor(currentUser?.id ?? ""),
}}
user={userConfig}
disabledExtensions={documentEditor}
aiHandler={{
menu: getAIMenu,
@@ -245,11 +250,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
},
}}
realtimeConfig={realtimeConfig}
user={{
id: currentUser?.id ?? "",
name: currentUser?.display_name ?? "",
color: generateRandomColor(currentUser?.id ?? ""),
}}
user={userConfig}
/>
)}
</div>
@@ -6,7 +6,6 @@ import { useAppTheme } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
projectId: string | null;
href: string;
title: string;
icon: JSX.Element;
@@ -14,7 +13,7 @@ type Props = {
};
export const FavoriteItemTitle: FC<Props> = observer((props) => {
const { projectId, href, title, icon, isSidebarCollapsed } = props;
const { href, title, icon, isSidebarCollapsed } = props;
// store hooks
const { toggleSidebar } = useAppTheme();
const { isMobile } = usePlatformOS();
@@ -24,10 +23,6 @@ export const FavoriteItemTitle: FC<Props> = observer((props) => {
"group/project-item cursor-pointer relative group w-full flex items-center justify-center gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90 truncate p-0 size-8 aspect-square mx-auto";
const handleOnClick = () => {
if (projectId) {
const projectItem = document.getElementById(`${projectId}`);
projectItem?.scrollIntoView({ behavior: "smooth" });
}
if (isMobile) toggleSidebar();
};
@@ -91,13 +91,7 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
<>
<FavoriteItemWrapper elementRef={elementRef} isMenuActive={isMenuActive} sidebarCollapsed={sidebarCollapsed}>
{!sidebarCollapsed && <FavoriteItemDragHandle isDragging={isDragging} sort_order={favorite.sort_order} />}
<FavoriteItemTitle
href={itemLink}
projectId={favorite.project_id}
icon={itemIcon}
title={itemTitle}
isSidebarCollapsed={!!sidebarCollapsed}
/>
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} isSidebarCollapsed={!!sidebarCollapsed} />
{!sidebarCollapsed && (
<FavoriteItemQuickAction
favorite={favorite}
@@ -287,7 +287,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
useEffect(() => {
if (URLProjectId === project.id) setIsProjectListOpen(true);
else setIsProjectListOpen(false);
}, [URLProjectId]);
const handleItemClick = () => {
@@ -15,9 +15,11 @@ import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useEventTracker, useUser, useUserPermissions } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web constants
import { SIDEBAR_USER_MENU_ITEMS } from "@/plane-web/constants/dashboard";
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// plane web helpers
import { isUserFeatureEnabled } from "@/plane-web/helpers/dashboard.helper";
export const SidebarUserMenu = observer(() => {
// store hooks
@@ -61,6 +63,7 @@ export const SidebarUserMenu = observer(() => {
>
{SIDEBAR_USER_MENU_ITEMS.map((link) => {
if (link.key === "drafts" && draftIssueCount === 0) return null;
if (!isUserFeatureEnabled(link.key)) return null;
return (
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
<Tooltip
+7 -1
View File
@@ -9,7 +9,7 @@ import { rootStore } from "@/lib/store-context";
// services
import { IssueService } from "@/services/issue/issue.service";
//
import { ARRAY_FIELDS } from "./utils/constants";
import { ARRAY_FIELDS, BOOLEAN_FIELDS } from "./utils/constants";
import { getSubIssuesWithDistribution } from "./utils/data.utils";
import createIndexes from "./utils/indexes";
import { addIssuesBulk, syncDeletesToLocal } from "./utils/load-issues";
@@ -75,6 +75,7 @@ export class Storage {
if (workspaceSlug !== this.workspaceSlug) {
this.reset();
}
try {
await startSpan({ name: "INIT_DB" }, async () => await this._initialize(workspaceSlug));
return true;
@@ -125,6 +126,7 @@ export class Storage {
return true;
} catch (error) {
this.status = "error";
this.db = null;
throw new Error(`Failed to initialize database worker: ${error}`);
}
};
@@ -467,5 +469,9 @@ export const formatLocalIssue = (issue: any) => {
ARRAY_FIELDS.forEach((field: string) => {
currIssue[field] = currIssue[field] ? JSON.parse(currIssue[field]) : [];
});
// Convert boolean fields to actual boolean values
BOOLEAN_FIELDS.forEach((field: string) => {
currIssue[field] = currIssue[field] === 1;
});
return currIssue as TIssue & { group_id?: string; total_issues: number; sub_group_id?: string };
};
+2
View File
@@ -1,5 +1,7 @@
export const ARRAY_FIELDS = ["label_ids", "assignee_ids", "module_ids"];
export const BOOLEAN_FIELDS = ["is_draft"];
export const GROUP_BY_MAP = {
state_id: "state_id",
priority: "priority",
+39 -32
View File
@@ -19,6 +19,44 @@ export const logError = (e: any) => {
};
export const logInfo = console.info;
export const addIssueToPersistanceLayer = async (issue: TIssue) => {
try {
const issuePartial = pick({ ...JSON.parse(JSON.stringify(issue)) }, [
"id",
"name",
"state_id",
"sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"sequence_id",
"project_id",
"parent_id",
"created_at",
"updated_at",
"created_by",
"updated_by",
"is_draft",
"archived_at",
"state__group",
"cycle_id",
"link_count",
"attachment_count",
"sub_issues_count",
"assignee_ids",
"label_ids",
"module_ids",
"type_id",
"description_html",
]);
await updateIssue({ ...issuePartial, is_local_update: 1 });
} catch (e) {
logError("Error while adding issue to db");
}
};
export const updatePersistentLayer = async (issueIds: string | string[]) => {
if (typeof issueIds === "string") {
issueIds = [issueIds];
@@ -28,38 +66,7 @@ export const updatePersistentLayer = async (issueIds: string | string[]) => {
const issue = rootStore.issue.issues.getIssueById(issueId);
if (issue) {
// JSON.parse(JSON.stringify(issue)) is used to remove the mobx observables
const issuePartial = pick({ ...dbIssue, ...JSON.parse(JSON.stringify(issue)) }, [
"id",
"name",
"state_id",
"sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"sequence_id",
"project_id",
"parent_id",
"created_at",
"updated_at",
"created_by",
"updated_by",
"is_draft",
"archived_at",
"state__group",
"cycle_id",
"link_count",
"attachment_count",
"sub_issues_count",
"assignee_ids",
"label_ids",
"module_ids",
"type_id",
"description_html",
]);
await updateIssue({ ...issuePartial, is_local_update: 1 });
addIssueToPersistanceLayer(issue);
}
});
};
+12 -4
View File
@@ -36,12 +36,20 @@ export class DBClass {
this.sqlite3 = SQLite.Factory(m);
const vfs = await MyVFS.create("plane", m);
this.sqlite3.vfs_register(vfs, true);
const db = await this.sqlite3.open_v2(
// Fallback in rare cases where the database is not initialized in time
const p = new Promise((resolve) => setTimeout(() => resolve(false), 2000));
const dbPromise = this.sqlite3.open_v2(
`${dbName}.sqlite3`,
this.sqlite3.OPEN_READWRITE | this.sqlite3.OPEN_CREATE,
"plane"
);
const db = await Promise.any([dbPromise, p]);
if (!db) {
throw new Error("Failed to initialize in time");
}
this.instance.db = db;
this.instance.exec = async (sql: string) => {
const rows: any[] = [];
@@ -58,7 +66,7 @@ export class DBClass {
}
runQuery(sql: string) {
return this.instance.exec(sql);
return this.instance?.exec?.(sql);
}
async exec(props: string | TQueryProps) {
@@ -103,14 +111,14 @@ export class DBClass {
}
if (sql === "COMMIT;" && this.tp) {
await this.instance.exec(sql);
await this.instance?.exec?.(sql);
if (this.tp.length > 0) {
const { resolve } = this.tpResolver.shift();
resolve();
}
return;
}
return await this.instance.exec(sql);
return await this.instance?.exec?.(sql);
}
async close() {
try {
@@ -92,7 +92,7 @@ export class InboxIssueService extends APIService {
});
}
async regeneratePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxIssue> {
async regeneratePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake-regenerate/`)
.then((response) => response?.data)
.catch((error) => {
@@ -59,7 +59,7 @@ export class WorkspaceDraftService extends APIService {
});
}
async moveIssue(workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>): Promise<void> {
async moveIssue(workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>): Promise<TIssue> {
return this.post(`/api/workspaces/${workspaceSlug}/draft-to-issue/${issueId}/`, payload)
.then((response) => response?.data)
.catch((error) => {
@@ -4,6 +4,8 @@ import { makeObservable, observable, runInAction, action } from "mobx";
import { TIssue, TInboxIssue, TInboxIssueStatus, TInboxDuplicateIssueDetails } from "@plane/types";
// helpers
import { EInboxIssueStatus } from "@/helpers/inbox.helper";
// local db
import { addIssueToPersistanceLayer } from "@/local-db/utils/utils";
// services
import { InboxIssueService } from "@/services/inbox";
import { IssueService } from "@/services/issue";
@@ -88,10 +90,16 @@ export class InboxIssueStore implements IInboxIssueStore {
try {
if (!this.issue.id) return;
const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
status: status,
});
runInAction(() => set(this, "status", inboxIssue?.status));
// If issue accepted sync issue to local db
if (status === EInboxIssueStatus.ACCEPTED) {
addIssueToPersistanceLayer(inboxIssue.issue);
}
} catch {
runInAction(() => set(this, "status", previousData.status));
}
+4 -1
View File
@@ -351,7 +351,10 @@ export class ProjectInboxStore implements IProjectInboxStore {
const form = await this.inboxIssueService.regeneratePublishForm(workspaceSlug, projectId);
if (form) {
runInAction(() => {
set(this.intakeForms, projectId, form);
set(this.intakeForms, projectId, {
...this.intakeForms[projectId],
anchor: form?.anchor,
});
});
}
} catch {
@@ -22,6 +22,8 @@ import {
import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
// helpers
import { getCurrentDateTimeInISO, convertToISODateString } from "@/helpers/date-time.helper";
// local-db
import { addIssueToPersistanceLayer } from "@/local-db/utils/utils";
// services
import workspaceDraftService from "@/services/issue/workspace_draft.service";
// types
@@ -59,7 +61,7 @@ export interface IWorkspaceDraftIssues {
payload: Partial<TWorkspaceDraftIssue | TIssue>
) => Promise<TWorkspaceDraftIssue | undefined>;
deleteIssue: (workspaceSlug: string, issueId: string) => Promise<void>;
moveIssue: (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>) => Promise<void>;
moveIssue: (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>) => Promise<TIssue>;
addCycleToIssue: (
workspaceSlug: string,
issueId: string,
@@ -348,6 +350,10 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
total_count: this.paginationInfo.total_count - 1,
});
}
// sync issue to local db
addIssueToPersistanceLayer(response);
// Update draft issue count in workspaceUserInfo
this.updateWorkspaceUserDraftIssueCount(workspaceSlug, -1);
});
+10
View File
@@ -13,6 +13,7 @@ import { CoreRootStore } from "../root.store";
export interface IProjectStore {
// observables
isUpdatingProject: boolean;
loader: boolean;
projectMap: {
[projectId: string]: TProject; // projectId: project Info
@@ -47,6 +48,7 @@ export interface IProjectStore {
export class ProjectStore implements IProjectStore {
// observables
isUpdatingProject: boolean = false;
loader: boolean = false;
projectMap: {
[projectId: string]: TProject; // projectId: project Info
@@ -63,6 +65,7 @@ export class ProjectStore implements IProjectStore {
constructor(_rootStore: CoreRootStore) {
makeObservable(this, {
// observables
isUpdatingProject: observable,
loader: observable.ref,
projectMap: observable,
// computed
@@ -380,13 +383,20 @@ export class ProjectStore implements IProjectStore {
const projectDetails = this.getProjectById(projectId);
runInAction(() => {
set(this.projectMap, [projectId], { ...projectDetails, ...data });
this.isUpdatingProject = true;
});
const response = await this.projectService.updateProject(workspaceSlug, projectId, data);
runInAction(() => {
this.isUpdatingProject = false;
});
return response;
} catch (error) {
console.log("Failed to create project from project store");
this.fetchProjects(workspaceSlug);
this.fetchProjectDetails(workspaceSlug, projectId);
runInAction(() => {
this.isUpdatingProject = false;
});
throw error;
}
};
+1
View File
@@ -0,0 +1 @@
export * from "ce/helpers/dashboard.helper";
+1
View File
@@ -0,0 +1 @@
export * from "ce/types/dashboard";