chore: squashed migration (#4634)

* chore: squashed migration

* chore: removed instance migraion

* chore: key changes

* chore: issue activity back migration

* dev: replaced estimate key with estimate id and replaced estimate type from number to string in issue

* chore: estimate point value field

* chore: estimate point activity

* chore: removed the unused function

* chore: resolved merge conflicts

* chore: deploy board keys changed

* chore: yarn lock file change

* chore: resolved frontend build

---------

Co-authored-by: guru_sainath <gurusainath007@gmail.com>
This commit is contained in:
Bavisetti Narayan 2024-06-05 17:47:48 +05:30 committed by GitHub
parent 028e70c4c1
commit 452e8f39ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 424 additions and 112 deletions

View File

@ -22,7 +22,7 @@ from plane.db.models import (
IssueProperty, IssueProperty,
Module, Module,
Project, Project,
ProjectDeployBoard, DeployBoard,
ProjectMember, ProjectMember,
State, State,
Workspace, Workspace,
@ -99,7 +99,7 @@ class ProjectAPIEndpoint(BaseAPIView):
) )
.annotate( .annotate(
is_deployed=Exists( is_deployed=Exists(
ProjectDeployBoard.objects.filter( DeployBoard.objects.filter(
project_id=OuterRef("pk"), project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
) )

View File

@ -30,7 +30,7 @@ from .project import (
ProjectIdentifierSerializer, ProjectIdentifierSerializer,
ProjectLiteSerializer, ProjectLiteSerializer,
ProjectMemberLiteSerializer, ProjectMemberLiteSerializer,
ProjectDeployBoardSerializer, DeployBoardSerializer,
ProjectMemberAdminSerializer, ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer, ProjectPublicMemberSerializer,
ProjectMemberRoleSerializer, ProjectMemberRoleSerializer,

View File

@ -13,7 +13,7 @@ from plane.db.models import (
ProjectMember, ProjectMember,
ProjectMemberInvite, ProjectMemberInvite,
ProjectIdentifier, ProjectIdentifier,
ProjectDeployBoard, DeployBoard,
ProjectPublicMember, ProjectPublicMember,
) )
@ -206,14 +206,14 @@ class ProjectMemberLiteSerializer(BaseSerializer):
read_only_fields = fields read_only_fields = fields
class ProjectDeployBoardSerializer(BaseSerializer): class DeployBoardSerializer(BaseSerializer):
project_details = ProjectLiteSerializer(read_only=True, source="project") project_details = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace" read_only=True, source="workspace"
) )
class Meta: class Meta:
model = ProjectDeployBoard model = DeployBoard
fields = "__all__" fields = "__all__"
read_only_fields = [ read_only_fields = [
"workspace", "workspace",

View File

@ -12,7 +12,7 @@ from plane.app.views import (
ProjectFavoritesViewSet, ProjectFavoritesViewSet,
UserProjectInvitationsViewset, UserProjectInvitationsViewset,
ProjectPublicCoverImagesEndpoint, ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet, DeployBoardViewSet,
UserProjectRolesEndpoint, UserProjectRolesEndpoint,
ProjectArchiveUnarchiveEndpoint, ProjectArchiveUnarchiveEndpoint,
) )
@ -157,7 +157,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/", "workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
ProjectDeployBoardViewSet.as_view( DeployBoardViewSet.as_view(
{ {
"get": "list", "get": "list",
"post": "create", "post": "create",
@ -167,7 +167,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
ProjectDeployBoardViewSet.as_view( DeployBoardViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",
"patch": "partial_update", "patch": "partial_update",

View File

@ -4,7 +4,7 @@ from .project.base import (
ProjectUserViewsEndpoint, ProjectUserViewsEndpoint,
ProjectFavoritesViewSet, ProjectFavoritesViewSet,
ProjectPublicCoverImagesEndpoint, ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet, DeployBoardViewSet,
ProjectArchiveUnarchiveEndpoint, ProjectArchiveUnarchiveEndpoint,
) )

View File

@ -28,7 +28,7 @@ from plane.app.views.base import BaseViewSet, BaseAPIView
from plane.app.serializers import ( from plane.app.serializers import (
ProjectSerializer, ProjectSerializer,
ProjectListSerializer, ProjectListSerializer,
ProjectDeployBoardSerializer, DeployBoardSerializer,
) )
from plane.app.permissions import ( from plane.app.permissions import (
@ -46,7 +46,7 @@ from plane.db.models import (
Module, Module,
Cycle, Cycle,
Inbox, Inbox,
ProjectDeployBoard, DeployBoard,
IssueProperty, IssueProperty,
Issue, Issue,
) )
@ -138,7 +138,7 @@ class ProjectViewSet(BaseViewSet):
) )
.annotate( .annotate(
is_deployed=Exists( is_deployed=Exists(
ProjectDeployBoard.objects.filter( DeployBoard.objects.filter(
project_id=OuterRef("pk"), project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
) )
@ -639,12 +639,12 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
return Response(files, status=status.HTTP_200_OK) return Response(files, status=status.HTTP_200_OK)
class ProjectDeployBoardViewSet(BaseViewSet): class DeployBoardViewSet(BaseViewSet):
permission_classes = [ permission_classes = [
ProjectMemberPermission, ProjectMemberPermission,
] ]
serializer_class = ProjectDeployBoardSerializer serializer_class = DeployBoardSerializer
model = ProjectDeployBoard model = DeployBoard
def get_queryset(self): def get_queryset(self):
return ( return (
@ -673,17 +673,17 @@ class ProjectDeployBoardViewSet(BaseViewSet):
}, },
) )
project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( project_deploy_board, _ = DeployBoard.objects.get_or_create(
anchor=f"{slug}/{project_id}", anchor=f"{slug}/{project_id}",
project_id=project_id, project_id=project_id,
) )
project_deploy_board.comments = comments
project_deploy_board.reactions = reactions
project_deploy_board.inbox = inbox project_deploy_board.inbox = inbox
project_deploy_board.votes = votes project_deploy_board.view_props = views
project_deploy_board.views = views project_deploy_board.is_votes_enabled = votes
project_deploy_board.is_comments_enabled = comments
project_deploy_board.is_reactions_enabled = reactions
project_deploy_board.save() project_deploy_board.save()
serializer = ProjectDeployBoardSerializer(project_deploy_board) serializer = DeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -28,6 +28,7 @@ from plane.db.models import (
Project, Project,
State, State,
User, User,
EstimatePoint,
) )
from plane.settings.redis import redis_instance from plane.settings.redis import redis_instance
from plane.utils.exception_logger import log_exception from plane.utils.exception_logger import log_exception
@ -448,21 +449,37 @@ def track_estimate_points(
if current_instance.get("estimate_point") != requested_data.get( if current_instance.get("estimate_point") != requested_data.get(
"estimate_point" "estimate_point"
): ):
old_estimate = (
EstimatePoint.objects.filter(
pk=current_instance.get("estimate_point")
).first()
if current_instance.get("estimate_point") is not None
else None
)
new_estimate = (
EstimatePoint.objects.filter(
pk=requested_data.get("estimate_point")
).first()
if requested_data.get("estimate_point") is not None
else None
)
issue_activities.append( issue_activities.append(
IssueActivity( IssueActivity(
issue_id=issue_id, issue_id=issue_id,
actor_id=actor_id, actor_id=actor_id,
verb="updated", verb="updated",
old_value=( old_identifier=(
current_instance.get("estimate_point") current_instance.get("estimate_point")
if current_instance.get("estimate_point") is not None if current_instance.get("estimate_point") is not None
else "" else None
), ),
new_value=( new_identifier=(
requested_data.get("estimate_point") requested_data.get("estimate_point")
if requested_data.get("estimate_point") is not None if requested_data.get("estimate_point") is not None
else "" else None
), ),
old_value=old_estimate.value if old_estimate else None,
new_value=new_estimate.value if new_estimate else None,
field="estimate_point", field="estimate_point",
project_id=project_id, project_id=project_id,
workspace_id=workspace_id, workspace_id=workspace_id,

View File

@ -0,0 +1,260 @@
# # Generated by Django 4.2.7 on 2024-05-24 09:47
# Python imports
import uuid
from uuid import uuid4
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import plane.db.models.deploy_board
def issue_estimate_point(apps, schema_editor):
Issue = apps.get_model("db", "Issue")
Project = apps.get_model("db", "Project")
EstimatePoint = apps.get_model("db", "EstimatePoint")
IssueActivity = apps.get_model("db", "IssueActivity")
updated_estimate_point = []
updated_issue_activity = []
# loop through all the projects
for project in Project.objects.filter(estimate__isnull=False):
estimate_points = EstimatePoint.objects.filter(
estimate=project.estimate, project=project
)
for issue_activity in IssueActivity.objects.filter(
field="estimate_point", project=project
):
if issue_activity.new_value:
new_identifier = estimate_points.filter(
key=issue_activity.new_value
).first().id
issue_activity.new_identifier = new_identifier
new_value = estimate_points.filter(
key=issue_activity.new_value
).first().value
issue_activity.new_value = new_value
if issue_activity.old_value:
old_identifier = estimate_points.filter(
key=issue_activity.old_value
).first().id
issue_activity.old_identifier = old_identifier
old_value = estimate_points.filter(
key=issue_activity.old_value
).first().value
issue_activity.old_value = old_value
updated_issue_activity.append(issue_activity)
for issue in Issue.objects.filter(
point__isnull=False, project=project
):
# get the estimate id for the corresponding estimate point in the issue
estimate = estimate_points.filter(key=issue.point).first()
issue.estimate_point = estimate
updated_estimate_point.append(issue)
Issue.objects.bulk_update(
updated_estimate_point, ["estimate_point"], batch_size=1000
)
IssueActivity.objects.bulk_update(
updated_issue_activity,
["new_value", "old_value", "new_identifier", "old_identifier"],
batch_size=1000,
)
def last_used_estimate(apps, schema_editor):
Project = apps.get_model("db", "Project")
Estimate = apps.get_model("db", "Estimate")
# Get all estimate ids used in projects
estimate_ids = Project.objects.filter(estimate__isnull=False).values_list(
"estimate", flat=True
)
# Update all matching estimates
Estimate.objects.filter(id__in=estimate_ids).update(last_used=True)
def populate_deploy_board(apps, schema_editor):
DeployBoard = apps.get_model("db", "DeployBoard")
ProjectDeployBoard = apps.get_model("db", "ProjectDeployBoard")
DeployBoard.objects.bulk_create(
[
DeployBoard(
entity_identifier=deploy_board.project_id,
project_id=deploy_board.project_id,
entity_name="project",
anchor=uuid4().hex,
is_comments_enabled=deploy_board.comments,
is_reactions_enabled=deploy_board.reactions,
inbox=deploy_board.inbox,
is_votes_enabled=deploy_board.votes,
view_props=deploy_board.views,
workspace_id=deploy_board.workspace_id,
created_at=deploy_board.created_at,
updated_at=deploy_board.updated_at,
created_by_id=deploy_board.created_by_id,
updated_by_id=deploy_board.updated_by_id,
)
for deploy_board in ProjectDeployBoard.objects.all()
],
batch_size=100,
)
class Migration(migrations.Migration):
dependencies = [
("db", "0066_account_id_token_cycle_logo_props_module_logo_props"),
]
operations = [
migrations.CreateModel(
name="DeployBoard",
fields=[
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("entity_identifier", models.UUIDField(null=True)),
(
"entity_name",
models.CharField(
choices=[
("project", "Project"),
("issue", "Issue"),
("module", "Module"),
("cycle", "Task"),
("page", "Page"),
("view", "View"),
],
max_length=30,
),
),
(
"anchor",
models.CharField(
db_index=True,
default=plane.db.models.deploy_board.get_anchor,
max_length=255,
unique=True,
),
),
("is_comments_enabled", models.BooleanField(default=False)),
("is_reactions_enabled", models.BooleanField(default=False)),
("is_votes_enabled", models.BooleanField(default=False)),
("view_props", models.JSONField(default=dict)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"inbox",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="board_inbox",
to="db.inbox",
),
),
(
"project",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
],
options={
"verbose_name": "Deploy Board",
"verbose_name_plural": "Deploy Boards",
"db_table": "deploy_boards",
"ordering": ("-created_at",),
"unique_together": {("entity_name", "entity_identifier")},
},
),
migrations.AddField(
model_name="estimate",
name="last_used",
field=models.BooleanField(default=False),
),
# Rename the existing field
migrations.RenameField(
model_name="issue",
old_name="estimate_point",
new_name="point",
),
# Add a new field with the original name as a foreign key
migrations.AddField(
model_name="issue",
name="estimate_point",
field=models.ForeignKey(
on_delete=django.db.models.deletion.SET_NULL,
related_name="issue_estimates",
to="db.EstimatePoint",
blank=True,
null=True,
),
),
migrations.AlterField(
model_name="estimate",
name="type",
field=models.CharField(default="categories", max_length=255),
),
migrations.AlterField(
model_name="estimatepoint",
name="value",
field=models.CharField(max_length=255),
),
migrations.RunPython(issue_estimate_point),
migrations.RunPython(last_used_estimate),
migrations.RunPython(populate_deploy_board),
]

View File

@ -4,6 +4,7 @@ from .asset import FileAsset
from .base import BaseModel from .base import BaseModel
from .cycle import Cycle, CycleFavorite, CycleIssue, CycleUserProperties from .cycle import Cycle, CycleFavorite, CycleIssue, CycleUserProperties
from .dashboard import Dashboard, DashboardWidget, Widget from .dashboard import Dashboard, DashboardWidget, Widget
from .deploy_board import DeployBoard
from .estimate import Estimate, EstimatePoint from .estimate import Estimate, EstimatePoint
from .exporter import ExporterHistory from .exporter import ExporterHistory
from .importer import Importer from .importer import Importer
@ -53,7 +54,6 @@ from .page import Page, PageFavorite, PageLabel, PageLog
from .project import ( from .project import (
Project, Project,
ProjectBaseModel, ProjectBaseModel,
ProjectDeployBoard,
ProjectFavorite, ProjectFavorite,
ProjectIdentifier, ProjectIdentifier,
ProjectMember, ProjectMember,

View File

@ -0,0 +1,53 @@
# Python imports
from uuid import uuid4
# Django imports
from django.db import models
# Module imports
from .workspace import WorkspaceBaseModel
def get_anchor():
return uuid4().hex
class DeployBoard(WorkspaceBaseModel):
TYPE_CHOICES = (
("project", "Project"),
("issue", "Issue"),
("module", "Module"),
("cycle", "Task"),
("page", "Page"),
("view", "View"),
)
entity_identifier = models.UUIDField(null=True)
entity_name = models.CharField(
max_length=30,
choices=TYPE_CHOICES,
)
anchor = models.CharField(
max_length=255, default=get_anchor, unique=True, db_index=True
)
is_comments_enabled = models.BooleanField(default=False)
is_reactions_enabled = models.BooleanField(default=False)
inbox = models.ForeignKey(
"db.Inbox",
related_name="board_inbox",
on_delete=models.SET_NULL,
null=True,
)
is_votes_enabled = models.BooleanField(default=False)
view_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the deploy board"""
return f"{self.entity_identifier} <{self.entity_name}>"
class Meta:
unique_together = ["entity_name", "entity_identifier"]
verbose_name = "Deploy Board"
verbose_name_plural = "Deploy Boards"
db_table = "deploy_boards"
ordering = ("-created_at",)

View File

@ -11,7 +11,8 @@ class Estimate(ProjectBaseModel):
description = models.TextField( description = models.TextField(
verbose_name="Estimate Description", blank=True verbose_name="Estimate Description", blank=True
) )
type = models.CharField(max_length=255, default="Categories") type = models.CharField(max_length=255, default="categories")
last_used = models.BooleanField(default=False)
def __str__(self): def __str__(self):
"""Return name of the estimate""" """Return name of the estimate"""
@ -35,7 +36,7 @@ class EstimatePoint(ProjectBaseModel):
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)] default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
) )
description = models.TextField(blank=True) description = models.TextField(blank=True)
value = models.CharField(max_length=20) value = models.CharField(max_length=255)
def __str__(self): def __str__(self):
"""Return name of the estimate""" """Return name of the estimate"""

View File

@ -119,11 +119,18 @@ class Issue(ProjectBaseModel):
blank=True, blank=True,
related_name="state_issue", related_name="state_issue",
) )
estimate_point = models.IntegerField( point = models.IntegerField(
validators=[MinValueValidator(0), MaxValueValidator(12)], validators=[MinValueValidator(0), MaxValueValidator(12)],
null=True, null=True,
blank=True, blank=True,
) )
estimate_point = models.ForeignKey(
"db.EstimatePoint",
on_delete=models.SET_NULL,
related_name="issue_estimates",
null=True,
blank=True,
)
name = models.CharField(max_length=255, verbose_name="Issue Name") name = models.CharField(max_length=255, verbose_name="Issue Name")
description = models.JSONField(blank=True, default=dict) description = models.JSONField(blank=True, default=dict)
description_html = models.TextField(blank=True, default="<p></p>") description_html = models.TextField(blank=True, default="<p></p>")

View File

@ -260,6 +260,8 @@ def get_default_views():
} }
# DEPRECATED TODO:
# used to get the old anchors for the project deploy boards
class ProjectDeployBoard(ProjectBaseModel): class ProjectDeployBoard(ProjectBaseModel):
anchor = models.CharField( anchor = models.CharField(
max_length=255, default=get_anchor, unique=True, db_index=True max_length=255, default=get_anchor, unique=True, db_index=True

View File

@ -18,7 +18,7 @@ from plane.db.models import (
State, State,
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
ProjectDeployBoard, DeployBoard,
) )
from plane.app.serializers import ( from plane.app.serializers import (
IssueSerializer, IssueSerializer,
@ -39,7 +39,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
] ]
def get_queryset(self): def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -59,7 +59,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
return InboxIssue.objects.none() return InboxIssue.objects.none()
def list(self, request, slug, project_id, inbox_id): def list(self, request, slug, project_id, inbox_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
@ -118,7 +118,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id, inbox_id): def create(self, request, slug, project_id, inbox_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
@ -189,7 +189,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, inbox_id, pk): def partial_update(self, request, slug, project_id, inbox_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
@ -256,7 +256,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
) )
def retrieve(self, request, slug, project_id, inbox_id, pk): def retrieve(self, request, slug, project_id, inbox_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
@ -280,7 +280,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, pk): def destroy(self, request, slug, project_id, inbox_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:

View File

@ -44,7 +44,7 @@ from plane.db.models import (
ProjectMember, ProjectMember,
IssueReaction, IssueReaction,
CommentReaction, CommentReaction,
ProjectDeployBoard, DeployBoard,
IssueVote, IssueVote,
ProjectPublicMember, ProjectPublicMember,
) )
@ -76,7 +76,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
try: try:
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -103,11 +103,11 @@ class IssueCommentPublicViewSet(BaseViewSet):
.distinct() .distinct()
).order_by("created_at") ).order_by("created_at")
return IssueComment.objects.none() return IssueComment.objects.none()
except ProjectDeployBoard.DoesNotExist: except DeployBoard.DoesNotExist:
return IssueComment.objects.none() return IssueComment.objects.none()
def create(self, request, slug, project_id, issue_id): def create(self, request, slug, project_id, issue_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -151,7 +151,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def partial_update(self, request, slug, project_id, issue_id, pk): def partial_update(self, request, slug, project_id, issue_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -184,7 +184,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, issue_id, pk): def destroy(self, request, slug, project_id, issue_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -221,7 +221,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
try: try:
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -236,11 +236,11 @@ class IssueReactionPublicViewSet(BaseViewSet):
.distinct() .distinct()
) )
return IssueReaction.objects.none() return IssueReaction.objects.none()
except ProjectDeployBoard.DoesNotExist: except DeployBoard.DoesNotExist:
return IssueReaction.objects.none() return IssueReaction.objects.none()
def create(self, request, slug, project_id, issue_id): def create(self, request, slug, project_id, issue_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -280,7 +280,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, issue_id, reaction_code): def destroy(self, request, slug, project_id, issue_id, reaction_code):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -319,7 +319,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
try: try:
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -334,11 +334,11 @@ class CommentReactionPublicViewSet(BaseViewSet):
.distinct() .distinct()
) )
return CommentReaction.objects.none() return CommentReaction.objects.none()
except ProjectDeployBoard.DoesNotExist: except DeployBoard.DoesNotExist:
return CommentReaction.objects.none() return CommentReaction.objects.none()
def create(self, request, slug, project_id, comment_id): def create(self, request, slug, project_id, comment_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -380,7 +380,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, comment_id, reaction_code): def destroy(self, request, slug, project_id, comment_id, reaction_code):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if not project_deploy_board.reactions: if not project_deploy_board.reactions:
@ -421,7 +421,7 @@ class IssueVotePublicViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
try: try:
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -434,7 +434,7 @@ class IssueVotePublicViewSet(BaseViewSet):
.filter(project_id=self.kwargs.get("project_id")) .filter(project_id=self.kwargs.get("project_id"))
) )
return IssueVote.objects.none() return IssueVote.objects.none()
except ProjectDeployBoard.DoesNotExist: except DeployBoard.DoesNotExist:
return IssueVote.objects.none() return IssueVote.objects.none()
def create(self, request, slug, project_id, issue_id): def create(self, request, slug, project_id, issue_id):
@ -513,7 +513,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
] ]
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
if not ProjectDeployBoard.objects.filter( if not DeployBoard.objects.filter(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
).exists(): ).exists():
return Response( return Response(

View File

@ -11,10 +11,10 @@ from rest_framework.permissions import AllowAny
# Module imports # Module imports
from .base import BaseAPIView from .base import BaseAPIView
from plane.app.serializers import ProjectDeployBoardSerializer from plane.app.serializers import DeployBoardSerializer
from plane.db.models import ( from plane.db.models import (
Project, Project,
ProjectDeployBoard, DeployBoard,
) )
@ -24,10 +24,10 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
] ]
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
serializer = ProjectDeployBoardSerializer(project_deploy_board) serializer = DeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@ -41,7 +41,7 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
Project.objects.filter(workspace__slug=slug) Project.objects.filter(workspace__slug=slug)
.annotate( .annotate(
is_public=Exists( is_public=Exists(
ProjectDeployBoard.objects.filter( DeployBoard.objects.filter(
workspace__slug=slug, project_id=OuterRef("pk") workspace__slug=slug, project_id=OuterRef("pk")
) )
) )

View File

@ -15,7 +15,7 @@ export type TIssue = {
priority: TIssuePriorities; priority: TIssuePriorities;
label_ids: string[]; label_ids: string[];
assignee_ids: string[]; assignee_ids: string[];
estimate_point: number | null; estimate_point: string | null;
sub_issues_count: number; sub_issues_count: number;
attachment_count: number; attachment_count: number;

View File

@ -24,7 +24,7 @@ import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon }
// helpers // helpers
import { renderFormattedDate } from "@/helpers/date-time.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper";
import { capitalizeFirstLetter } from "@/helpers/string.helper"; import { capitalizeFirstLetter } from "@/helpers/string.helper";
import { useEstimate, useLabel } from "@/hooks/store"; import { useLabel } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// types // types
@ -97,22 +97,6 @@ const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; works
); );
}); });
const EstimatePoint = observer((props: { point: string }) => {
const { point } = props;
const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate();
const currentPoint = Number(point) + 1;
const estimateValue = getEstimatePointValue(Number(point), null);
return (
<span className="font-medium text-custom-text-100 whitespace-nowrap">
{areEstimatesEnabledForCurrentProject
? estimateValue
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
</span>
);
});
const inboxActivityMessage = { const inboxActivityMessage = {
declined: { declined: {
showIssue: "declined issue", showIssue: "declined issue",
@ -267,7 +251,7 @@ const activityDetails: {
else else
return ( return (
<> <>
set the estimate point to <EstimatePoint point={activity.new_value} /> set the estimate point to {activity.new_value}
{showIssue && ( {showIssue && (
<> <>
{" "} {" "}

View File

@ -19,15 +19,15 @@ type Props = TDropdownProps & {
button?: ReactNode; button?: ReactNode;
dropdownArrow?: boolean; dropdownArrow?: boolean;
dropdownArrowClassName?: string; dropdownArrowClassName?: string;
onChange: (val: number | null) => void; onChange: (val: string | null) => void;
onClose?: () => void; onClose?: () => void;
projectId: string; projectId: string;
value: number | null; value: string | null;
}; };
type DropdownOptions = type DropdownOptions =
| { | {
value: number | null; value: string | null;
query: string; query: string;
content: JSX.Element; content: JSX.Element;
}[] }[]
@ -80,7 +80,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
const activeEstimate = getProjectActiveEstimateDetails(projectId); const activeEstimate = getProjectActiveEstimateDetails(projectId);
const options: DropdownOptions = sortBy(activeEstimate?.points ?? [], "key")?.map((point) => ({ const options: DropdownOptions = sortBy(activeEstimate?.points ?? [], "key")?.map((point) => ({
value: point.key, value: point.id,
query: `${point?.value}`, query: `${point?.value}`,
content: ( content: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -120,7 +120,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
setQuery, setQuery,
}); });
const dropdownOnChange = (val: number | null) => { const dropdownOnChange = (val: string | null) => {
onChange(val); onChange(val);
handleClose(); handleClose();
}; };

View File

@ -2,7 +2,7 @@ import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Triangle } from "lucide-react"; import { Triangle } from "lucide-react";
// hooks // hooks
import { useEstimate, useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// components // components
import { IssueActivityBlockComponent, IssueLink } from "./"; import { IssueActivityBlockComponent, IssueLink } from "./";
@ -14,15 +14,11 @@ export const IssueEstimateActivity: FC<TIssueEstimateActivity> = observer((props
const { const {
activity: { getActivityById }, activity: { getActivityById },
} = useIssueDetail(); } = useIssueDetail();
const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate();
const activity = getActivityById(activityId); const activity = getActivityById(activityId);
if (!activity) return <></>; if (!activity) return <></>;
const estimateValue = getEstimatePointValue(Number(activity.new_value), null);
const currentPoint = Number(activity.new_value) + 1;
return ( return (
<IssueActivityBlockComponent <IssueActivityBlockComponent
icon={<Triangle size={14} color="#6b7280" aria-hidden="true" />} icon={<Triangle size={14} color="#6b7280" aria-hidden="true" />}
@ -31,15 +27,7 @@ export const IssueEstimateActivity: FC<TIssueEstimateActivity> = observer((props
> >
<> <>
{activity.new_value ? `set the estimate point to ` : `removed the estimate point `} {activity.new_value ? `set the estimate point to ` : `removed the estimate point `}
{activity.new_value && ( {activity.new_value ? activity.new_value : activity?.old_value || ""}
<>
<span className="font-medium text-custom-text-100">
{areEstimatesEnabledForCurrentProject
? estimateValue
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
</span>
</>
)}
{showIssue && (activity.new_value ? ` to ` : ` from `)} {showIssue && (activity.new_value ? ` to ` : ` from `)}
{showIssue && <IssueLink activityId={activityId} />}. {showIssue && <IssueLink activityId={activityId} />}.
</> </>

View File

@ -318,7 +318,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<span>Estimate</span> <span>Estimate</span>
</div> </div>
<EstimateDropdown <EstimateDropdown
value={issue?.estimate_point !== null ? issue.estimate_point : null} value={issue?.estimate_point != null ? issue.estimate_point : null}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })}
projectId={projectId} projectId={projectId}
disabled={!isEditable} disabled={!isEditable}

View File

@ -220,7 +220,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
); );
}; };
const handleEstimate = (value: number | null) => { const handleEstimate = (value: string | null) => {
updateIssue && updateIssue &&
updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => { updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => {
captureIssueEvent({ captureIssueEvent({

View File

@ -495,7 +495,7 @@ export const handleGroupDragDrop = async (
// update updatedIssue values based on the source and destination groupIds // update updatedIssue values based on the source and destination groupIds
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) { if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy]; const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy];
let groupValue = clone(sourceIssue[groupKey]); let groupValue: any = clone(sourceIssue[groupKey]);
// If groupValues is an array, remove source groupId and add destination groupId // If groupValues is an array, remove source groupId and add destination groupId
if (Array.isArray(groupValue)) { if (Array.isArray(groupValue)) {
@ -515,7 +515,7 @@ export const handleGroupDragDrop = async (
// update updatedIssue values based on the source and destination subGroupIds // update updatedIssue values based on the source and destination subGroupIds
if (subGroupBy && source.subGroupId && destination.subGroupId && source.subGroupId !== destination.subGroupId) { if (subGroupBy && source.subGroupId && destination.subGroupId && source.subGroupId !== destination.subGroupId) {
const subGroupKey = ISSUE_FILTER_DEFAULT_DATA[subGroupBy]; const subGroupKey = ISSUE_FILTER_DEFAULT_DATA[subGroupBy];
let subGroupValue = clone(sourceIssue[subGroupKey]); let subGroupValue: any = clone(sourceIssue[subGroupKey]);
// If subGroupValue is an array, remove source subGroupId and add destination subGroupId // If subGroupValue is an array, remove source subGroupId and add destination subGroupId
if (Array.isArray(subGroupValue)) { if (Array.isArray(subGroupValue)) {

View File

@ -19,7 +19,7 @@ export interface IEstimateStore {
activeEstimateDetails: IEstimate | null; activeEstimateDetails: IEstimate | null;
// computed actions // computed actions
areEstimatesEnabledForProject: (projectId: string) => boolean; areEstimatesEnabledForProject: (projectId: string) => boolean;
getEstimatePointValue: (estimateKey: number | null, projectId: string | null) => string; getEstimatePointValue: (estimateKey: string | null, projectId: string | null) => string;
getProjectEstimateById: (estimateId: string) => IEstimate | null; getProjectEstimateById: (estimateId: string) => IEstimate | null;
getProjectActiveEstimateDetails: (projectId: string) => IEstimate | null; getProjectActiveEstimateDetails: (projectId: string) => IEstimate | null;
// fetch actions // fetch actions
@ -110,10 +110,10 @@ export class EstimateStore implements IEstimateStore {
/** /**
* @description returns the point value for the given estimate key to display in the UI * @description returns the point value for the given estimate key to display in the UI
*/ */
getEstimatePointValue = computedFn((estimateKey: number | null, projectId: string | null) => { getEstimatePointValue = computedFn((estimateKey: string | null, projectId: string | null) => {
if (estimateKey === null) return "None"; if (estimateKey === null) return "None";
const activeEstimate = projectId ? this.getProjectActiveEstimateDetails(projectId) : this.activeEstimateDetails; const activeEstimate = projectId ? this.getProjectActiveEstimateDetails(projectId) : this.activeEstimateDetails;
return activeEstimate?.points?.find((point) => point.key === estimateKey)?.value || "None"; return activeEstimate?.points?.find((point) => point.id === estimateKey)?.value || "None";
}); });
/** /**

View File

@ -4,10 +4,10 @@ import sortBy from "lodash/sortBy";
import uniq from "lodash/uniq"; import uniq from "lodash/uniq";
import update from "lodash/update"; import update from "lodash/update";
import { action, makeObservable, observable, runInAction } from "mobx"; import { action, makeObservable, observable, runInAction } from "mobx";
import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types";
// services // services
import { IssueActivityService } from "@/services/issue"; import { IssueActivityService } from "@/services/issue";
// types // types
import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types";
import { IIssueDetail } from "./root.store"; import { IIssueDetail } from "./root.store";
export type TActivityLoader = "fetch" | "mutate" | undefined; export type TActivityLoader = "fetch" | "mutate" | undefined;
@ -117,10 +117,10 @@ export class IssueActivityStore implements IIssueActivityStore {
this.loader = loaderType; this.loader = loaderType;
let props = {}; let props = {};
const _activityIds = this.getActivitiesByIssueId(issueId); const currentActivityIds = this.getActivitiesByIssueId(issueId);
if (_activityIds && _activityIds.length > 0) { if (currentActivityIds && currentActivityIds.length > 0) {
const _activity = this.getActivityById(_activityIds[_activityIds.length - 1]); const currentActivity = this.getActivityById(currentActivityIds[currentActivityIds.length - 1]);
if (_activity) props = { created_at__gt: _activity.created_at }; if (currentActivity) props = { created_at__gt: currentActivity.created_at };
} }
const activities = await this.issueActivityService.getIssueActivities(workspaceSlug, projectId, issueId, props); const activities = await this.issueActivityService.getIssueActivities(workspaceSlug, projectId, issueId, props);
@ -128,9 +128,9 @@ export class IssueActivityStore implements IIssueActivityStore {
const activityIds = activities.map((activity) => activity.id); const activityIds = activities.map((activity) => activity.id);
runInAction(() => { runInAction(() => {
update(this.activities, issueId, (_activityIds) => { update(this.activities, issueId, (currentActivityIds) => {
if (!_activityIds) return activityIds; if (!currentActivityIds) return activityIds;
return uniq(concat(_activityIds, activityIds)); return uniq(concat(currentActivityIds, activityIds));
}); });
activities.forEach((activity) => { activities.forEach((activity) => {
set(this.activityMap, activity.id, activity); set(this.activityMap, activity.id, activity);