Compare commits

...

14 Commits

Author SHA1 Message Date
rahulramesha 957e981168 use common getIssues from issue service instead of multiple different services for modules and cycles 2024-07-29 18:41:46 +05:30
Nikhil f48bc5a876 fix: google auth integrity error (#5229) 2024-07-29 14:29:45 +05:30
Bavisetti Narayan 10e9122c1d [WEB-2092] chore: soft delete operation (#5244)
* chore: soft delete opration

* chore: migration files

* chore: celery time change

* chore: changed the deletion time
2024-07-29 14:29:08 +05:30
rahulramesha d5cbe3283b remove issue from cycle while changing cycle (#5246) 2024-07-29 13:26:27 +05:30
Anmol Singh Bhatia ae931f8172 [WEB-2054] fix: kanban layout loader enhancements and issue count alignment (#5232)
* fix: kanban layout issue count alignment

* fix: kanban layout loader spacing and padding
2024-07-29 13:23:12 +05:30
Anmol Singh Bhatia a8c6483c60 fix: profile display name error message (#5237) 2024-07-29 11:35:16 +05:30
Anmol Singh Bhatia 9c761a614f fix: inbox filters checkbox (#5239) 2024-07-29 11:34:36 +05:30
Anmol Singh Bhatia adf88a0f13 fix: issue link modal preloadedData reset (#5240) 2024-07-29 11:33:25 +05:30
Aaryan Khandelwal 5d2983d027 fix: creation of new todo list item in comments (#5242) 2024-07-29 11:29:09 +05:30
Anmol Singh Bhatia 8339daa3ee fix: member role edit validation (#5236) 2024-07-29 11:28:23 +05:30
Aaryan Khandelwal 4a9e09a54a fix: image outline on load (#5230) 2024-07-29 11:24:23 +05:30
Bavisetti Narayan 2c609670c8 [WEB-2043] chore: updated permissions for delete operation (#5231)
* chore: added permission for delete operation

* chore: added permission for external apis

* chore: condition changes

* chore: minor changes
2024-07-26 16:42:51 +05:30
Akshita Goyal dfcba4dfc1 fix: revoked issue height change (#5238) 2024-07-26 13:38:26 +05:30
Manish Gupta d0e68cdcfb chore: self host custom build (#5228)
* removed code build process from install script

* fixes in install.sh

* fixed docker-compose.yaml

* wip

* sync env files during upgrade

* updated variables.env

* updated readme

* Update deploy/selfhost/install.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* implemented codacy suggestions

* implemented codacy suggestions

* Update deploy/selfhost/install.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update deploy/selfhost/install.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update deploy/selfhost/install.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* update codacy suggestions

* coderabbit suggestion

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-07-25 20:35:51 +05:30
45 changed files with 1295 additions and 343 deletions
+18 -3
View File
@@ -34,6 +34,7 @@ from plane.db.models import (
Project,
IssueAttachment,
IssueLink,
ProjectMember,
)
from plane.utils.analytics_plot import burndown_plot
@@ -363,14 +364,28 @@ class CycleAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if cycle.owned_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or creator can delete the cycle"},
status=status.HTTP_403_FORBIDDEN,
)
cycle_issues = list(
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk")
).values_list("issue", flat=True)
)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
issue_activity.delay(
type="cycle.activity.deleted",
+16 -19
View File
@@ -390,29 +390,26 @@ class InboxIssueAPIEndpoint(BaseAPIView):
inbox_id=inbox.id,
)
# Get the project member
project_member = ProjectMember.objects.get(
workspace__slug=slug,
project_id=project_id,
member=request.user,
is_active=True,
)
# Check the inbox issue created
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot delete inbox issue"},
status=status.HTTP_400_BAD_REQUEST,
)
# Check the issue status
if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
Issue.objects.filter(
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=issue_id
).delete()
).first()
if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or creator can delete the issue"},
status=status.HTTP_403_FORBIDDEN,
)
issue.delete()
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+18 -2
View File
@@ -310,11 +310,14 @@ class IssueAPIEndpoint(BaseAPIView):
serializer.save()
# Refetch the issue
issue = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]).first()
issue = Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
pk=serializer.data["id"],
).first()
issue.created_at = request.data.get("created_at")
issue.save(update_fields=["created_at"])
# Track the issue
issue_activity.delay(
type="issue.activity.created",
@@ -386,6 +389,19 @@ class IssueAPIEndpoint(BaseAPIView):
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or creator can delete the issue"},
status=status.HTTP_403_FORBIDDEN,
)
current_instance = json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder
)
+15
View File
@@ -27,6 +27,7 @@ from plane.db.models import (
ModuleIssue,
ModuleLink,
Project,
ProjectMember,
)
from .base import BaseAPIView
@@ -265,6 +266,20 @@ class ModuleAPIEndpoint(BaseAPIView):
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if module.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or creator can delete the module"},
status=status.HTTP_403_FORBIDDEN,
)
module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list(
"issue", flat=True
+18 -3
View File
@@ -47,6 +47,7 @@ from plane.db.models import (
Label,
User,
Project,
ProjectMember,
)
from plane.utils.analytics_plot import burndown_plot
@@ -1039,14 +1040,28 @@ class CycleViewSet(BaseViewSet):
)
def destroy(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if cycle.owned_by_id != request.user.id and not (
ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or owner can delete the cycle"},
status=status.HTTP_403_FORBIDDEN,
)
cycle_issues = list(
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk")
).values_list("issue", flat=True)
)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
issue_activity.delay(
type="cycle.activity.deleted",
+16 -17
View File
@@ -553,28 +553,27 @@ class InboxIssueViewSet(BaseViewSet):
project_id=project_id,
inbox_id=inbox_id,
)
# Get the project member
project_member = ProjectMember.objects.get(
workspace__slug=slug,
project_id=project_id,
member=request.user,
is_active=True,
)
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot delete inbox issue"},
status=status.HTTP_400_BAD_REQUEST,
)
# Check the issue status
if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
Issue.objects.filter(
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=issue_id
).delete()
).first()
if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or creator can delete the issue"},
status=status.HTTP_403_FORBIDDEN,
)
issue.delete()
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+30
View File
@@ -44,6 +44,7 @@ from plane.db.models import (
IssueReaction,
IssueSubscriber,
Project,
ProjectMember,
)
from plane.utils.grouper import (
issue_group_values,
@@ -165,6 +166,7 @@ class IssueListEndpoint(BaseAPIView):
"link_count",
"is_draft",
"archived_at",
"deleted_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
@@ -399,6 +401,7 @@ class IssueViewSet(BaseViewSet):
"link_count",
"is_draft",
"archived_at",
"deleted_at",
)
.first()
)
@@ -549,6 +552,20 @@ class IssueViewSet(BaseViewSet):
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or creator can delete the issue"},
status=status.HTTP_403_FORBIDDEN,
)
issue.delete()
issue_activity.delay(
type="issue.activity.deleted",
@@ -602,6 +619,19 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
]
def delete(self, request, slug, project_id):
if ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists():
return Response(
{"error": "Only admin can perform this action"},
status=status.HTTP_403_FORBIDDEN,
)
issue_ids = request.data.get("issue_ids", [])
if not len(issue_ids):
+14
View File
@@ -40,6 +40,7 @@ from plane.db.models import (
IssueReaction,
IssueSubscriber,
Project,
ProjectMember,
)
from plane.utils.grouper import (
issue_group_values,
@@ -380,6 +381,19 @@ class IssueDraftViewSet(BaseViewSet):
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or creator can delete the issue"},
status=status.HTTP_403_FORBIDDEN,
)
issue.delete()
issue_activity.delay(
type="issue_draft.activity.deleted",
+16
View File
@@ -48,6 +48,7 @@ from plane.db.models import (
ModuleLink,
ModuleUserProperties,
Project,
ProjectMember,
)
from plane.utils.analytics_plot import burndown_plot
from plane.utils.user_timezone_converter import user_timezone_converter
@@ -737,6 +738,21 @@ class ModuleViewSet(BaseViewSet):
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if module.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or creator can delete the module"},
status=status.HTTP_403_FORBIDDEN,
)
module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list(
"issue", flat=True
+14
View File
@@ -333,6 +333,20 @@ class PageViewSet(BaseViewSet):
pk=pk, workspace__slug=slug, projects__id=project_id
)
if not page.owned_by_id != request.user.id and not (
ProjectMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
project_id=project_id,
is_active=True,
).exists()
):
return Response(
{"error": "Only admin or owner can delete the page"},
status=status.HTTP_403_FORBIDDEN,
)
# only the owner and admin can delete the page
if (
ProjectMember.objects.filter(
+24 -8
View File
@@ -116,6 +116,20 @@ class WorkspaceViewViewSet(BaseViewSet):
pk=pk,
workspace__slug=slug,
)
if not (
WorkspaceMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=20,
is_active=True,
).exists()
and workspace_view.owned_by_id != request.user.id
):
return Response(
{"error": "You do not have permission to delete this view"},
status=status.HTTP_403_FORBIDDEN,
)
workspace_member = WorkspaceMember.objects.filter(
workspace__slug=slug,
member=request.user,
@@ -412,14 +426,16 @@ class IssueViewViewSet(BaseViewSet):
project_id=project_id,
workspace__slug=slug,
)
project_member = ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project_id,
member=request.user,
role=20,
is_active=True,
)
if project_member.exists() or project_view.owned_by == request.user:
if (
ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project_id,
member=request.user,
role=20,
is_active=True,
).exists()
or project_view.owned_by_id == request.user.id
):
project_view.delete()
else:
return Response(
@@ -100,10 +100,8 @@ class OauthAdapter(Adapter):
account, created = Account.objects.update_or_create(
user=user,
provider=self.provider,
provider_account_id=self.user_data.get("user").get("provider_id"),
defaults={
"provider_account_id": self.user_data.get("user").get(
"provider_id"
),
"access_token": self.token_data.get("access_token"),
"refresh_token": self.token_data.get("refresh_token", None),
"access_token_expired_at": self.token_data.get(
+158
View File
@@ -0,0 +1,158 @@
# Django imports
from django.utils import timezone
from django.apps import apps
from django.core.exceptions import ObjectDoesNotExist
# Third party imports
from celery import shared_task
@shared_task
def soft_delete_related_objects(
app_label, model_name, instance_pk, using=None
):
model_class = apps.get_model(app_label, model_name)
instance = model_class.all_objects.get(pk=instance_pk)
related_fields = instance._meta.get_fields()
for field in related_fields:
if field.one_to_many or field.one_to_one or field.many_to_many:
try:
if field.one_to_many or field.many_to_many:
related_objects = getattr(instance, field.name).all()
elif field.one_to_one:
related_object = getattr(instance, field.name)
related_objects = (
[related_object] if related_object is not None else []
)
for obj in related_objects:
if obj:
obj.deleted_at = timezone.now()
obj.save(using=using)
except ObjectDoesNotExist:
pass
# @shared_task
def restore_related_objects(app_label, model_name, instance_pk, using=None):
pass
@shared_task
def hard_delete():
from plane.db.models import (
Workspace,
Project,
Cycle,
Module,
Issue,
Page,
IssueView,
Label,
State,
IssueActivity,
IssueComment,
IssueLink,
IssueReaction,
UserFavorite,
ModuleIssue,
CycleIssue,
Estimate,
EstimatePoint
)
# check delete workspace
_ = Workspace.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
# check delete project
_ = Project.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
# check delete cycle
_ = Cycle.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
# check delete module
_ = Module.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
# check delete issue
_ = Issue.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
# check delete page
_ = Page.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
# check delete view
_ = IssueView.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
# check delete label
_ = Label.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
# check delete state
_ = State.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
_ = IssueActivity.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
_ = IssueComment.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
_ = IssueLink.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
_ = IssueReaction.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
_ = UserFavorite.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
_ = ModuleIssue.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
_ = CycleIssue.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
_ = Estimate.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
_ = EstimatePoint.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
# at last, check for every thing which ever is left and delete it
# Get all Django models
all_models = apps.get_models()
# Iterate through all models
for model in all_models:
# Check if the model has a 'deleted_at' field
if hasattr(model, "deleted_at"):
# Get all instances where 'deleted_at' is greater than 30 days ago
_ = model.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=30)
).delete()
return
@@ -221,7 +221,6 @@ def notifications(
else None
)
if type not in [
"issue.activity.deleted",
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
+4
View File
@@ -36,6 +36,10 @@ app.conf.beat_schedule = {
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-hard-delete": {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0),
},
}
# Load task modules from all registered Django app configs.
@@ -0,0 +1,423 @@
# Generated by Django 4.2.11 on 2024-07-26 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0072_issueattachment_external_id_and_more'),
]
operations = [
migrations.AddField(
model_name='analyticview',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='apiactivitylog',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='apitoken',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='commentreaction',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='cycle',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='cyclefavorite',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='cycleissue',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='cycleuserproperties',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='dashboard',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='dashboardwidget',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='deployboard',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='emailnotificationlog',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='estimate',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='estimatepoint',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='exporterhistory',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='fileasset',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='githubcommentsync',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='githubissuesync',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='githubrepository',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='githubrepositorysync',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='globalview',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='importer',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='inbox',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='inboxissue',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='integration',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issue',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issueactivity',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issueassignee',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issueattachment',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issueblocker',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuecomment',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuelabel',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuelink',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuemention',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuereaction',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuerelation',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuesequence',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuesubscriber',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuetype',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issueuserproperty',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issueview',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issueviewfavorite',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='issuevote',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='label',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='module',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='modulefavorite',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='moduleissue',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='modulelink',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='modulemember',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='moduleuserproperties',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='notification',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='page',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='pageblock',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='pagefavorite',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='pagelabel',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='pagelog',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='pageversion',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='project',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='projectdeployboard',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='projectfavorite',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='projectidentifier',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='projectmember',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='projectmemberinvite',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='projectpage',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='projectpublicmember',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='slackprojectsync',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='socialloginconnection',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='state',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='team',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='teammember',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='teampage',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='userfavorite',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='usernotificationpreference',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='userrecentvisit',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='webhook',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='webhooklog',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='workspace',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='workspaceintegration',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='workspacemember',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='workspacememberinvite',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='workspacetheme',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='workspaceuserproperties',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
]
+43 -3
View File
@@ -1,7 +1,9 @@
# Python imports
# Django imports
from django.db import models
from django.utils import timezone
# Module imports
from plane.bgtasks.deletion_task import soft_delete_related_objects
class TimeAuditModel(models.Model):
@@ -41,7 +43,45 @@ class UserAuditModel(models.Model):
abstract = True
class AuditModel(TimeAuditModel, UserAuditModel):
class SoftDeletionManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(deleted_at__isnull=True)
class SoftDeleteModel(models.Model):
"""To soft delete records"""
deleted_at = models.DateTimeField(
verbose_name="Deleted At",
null=True,
blank=True,
)
objects = SoftDeletionManager()
all_objects = models.Manager()
class Meta:
abstract = True
def delete(self, using=None, soft=True, *args, **kwargs):
if soft:
# Soft delete the current instance
self.deleted_at = timezone.now()
self.save(using=using)
soft_delete_related_objects.delay(
self._meta.app_label,
self._meta.model_name,
self.pk,
using=using,
)
else:
# Perform hard delete if soft deletion is not enabled
return super().delete(using=using, *args, **kwargs)
class AuditModel(TimeAuditModel, UserAuditModel, SoftDeleteModel):
"""To path when the record was created and last modified"""
class Meta:
+1
View File
@@ -116,6 +116,7 @@ class CycleIssue(ProjectBaseModel):
return f"{self.cycle}"
# DEPRECATED TODO: - Remove in next release
class CycleFavorite(ProjectBaseModel):
"""_summary_
CycleFavorite (model): To store all the cycle favorite of the user
+1
View File
@@ -89,6 +89,7 @@ class IssueManager(models.Manager):
| models.Q(issue_inbox__status=2)
| models.Q(issue_inbox__isnull=True)
)
.filter(deleted_at__isnull=True)
.filter(state__is_triage=False)
.exclude(archived_at__isnull=False)
.exclude(project__archived_at__isnull=False)
+1
View File
@@ -169,6 +169,7 @@ class ModuleLink(ProjectBaseModel):
return f"{self.module.name} {self.url}"
# DEPRECATED TODO: - Remove in next release
class ModuleFavorite(ProjectBaseModel):
"""_summary_
ModuleFavorite (model): To store all the module favorite of the user
+2
View File
@@ -119,6 +119,7 @@ class PageLog(BaseModel):
return f"{self.page.name} {self.entity_name}"
# DEPRECATED TODO: - Remove in next release
class PageBlock(ProjectBaseModel):
page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, related_name="blocks"
@@ -175,6 +176,7 @@ class PageBlock(ProjectBaseModel):
return f"{self.page.name} <{self.name}>"
# DEPRECATED TODO: - Remove in next release
class PageFavorite(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
+1
View File
@@ -230,6 +230,7 @@ class ProjectIdentifier(AuditModel):
ordering = ("-created_at",)
# DEPRECATED TODO: - Remove in next release
class ProjectFavorite(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
+1 -1
View File
@@ -52,7 +52,6 @@ def get_default_display_properties():
"updated_on": True,
}
# DEPRECATED TODO: - Remove in next release
class GlobalView(BaseModel):
workspace = models.ForeignKey(
@@ -142,6 +141,7 @@ class IssueView(WorkspaceBaseModel):
return f"{self.name} <{self.project.name}>"
# DEPRECATED TODO: - Remove in next release
class IssueViewFavorite(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
@@ -0,0 +1,33 @@
# Generated by Django 4.2.11 on 2024-07-26 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('license', '0003_alter_changelog_title_alter_changelog_version_and_more'),
]
operations = [
migrations.AddField(
model_name='changelog',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='instance',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='instanceadmin',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
migrations.AddField(
model_name='instanceconfiguration',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
),
]
+4 -21
View File
@@ -10,7 +10,7 @@ Let's get started!
<details>
<summary>Option 1 - Using Cloud Server</summary>
<p>Best way to start is to create EC2 maching on AWS. It must of minimum t3.medium/t3a/medium</p>
<p>Best way to start is to create EC2 machine on AWS. It must have minimum of 2vCPU and 4GB RAM.</p>
<p>Run the below command to install docker engine.</p>
`curl -fsSL https://get.docker.com | sh -`
@@ -67,23 +67,6 @@ curl -fsSL -o setup.sh https://raw.githubusercontent.com/makeplane/plane/master/
chmod +x setup.sh
```
<details>
<summary>Downloading Preview Release</summary>
```
mkdir plane-selfhost
cd plane-selfhost
export RELEASE=preview
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/install.sh | sed 's@BRANCH=master@BRANCH='"$RELEASE"'@' > setup.sh
chmod +x setup.sh
```
</details>
---
### Proceed with setup
@@ -114,7 +97,7 @@ This will create a create a folder `plane-app` or `plane-app-preview` (in case o
- `docker-compose.yaml`
- `plane.env`
Again the `options [1-7]` will be popped up and this time hit `7` to exit.
Again the `options [1-8]` will be popped up and this time hit `8` to exit.
---
@@ -236,7 +219,7 @@ Select a Action you want to perform:
Action [2]: 5
```
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `variables-upgrade.env`. Here system will not replace `.env` with the new one.
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`.
You must expect the below message
@@ -244,7 +227,7 @@ You must expect the below message
Once done, choose `8` to exit from prompt.
> It is very important for you to compare the 2 files `variables-upgrade.env` and `.env`. Copy the newly added variable from downloaded file to `.env` and set the expected values.
> It is very important for you to validate the `plane.env` for the new changes.
Once done with making changes in `plane.env` file, jump on to `Start Server`
-2
View File
@@ -1,5 +1,3 @@
version: "3.8"
services:
web:
image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest}
+9 -8
View File
@@ -44,7 +44,7 @@ services:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: ${PULL_POLICY:-always}
pull_policy: if_not_present
restart: unless-stopped
command: node web/server.js web
deploy:
@@ -57,7 +57,7 @@ services:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: ${PULL_POLICY:-always}
pull_policy: if_not_present
restart: unless-stopped
command: node space/server.js space
deploy:
@@ -71,7 +71,7 @@ services:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: ${PULL_POLICY:-always}
pull_policy: if_not_present
restart: unless-stopped
command: node admin/server.js admin
deploy:
@@ -84,7 +84,7 @@ services:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: ${PULL_POLICY:-always}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-api.sh
deploy:
@@ -99,7 +99,7 @@ services:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: ${PULL_POLICY:-always}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-worker.sh
volumes:
@@ -113,7 +113,7 @@ services:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: ${PULL_POLICY:-always}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-beat.sh
volumes:
@@ -127,7 +127,7 @@ services:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: ${PULL_POLICY:-always}
pull_policy: if_not_present
restart: "no"
command: ./bin/docker-entrypoint-migrator.sh
volumes:
@@ -167,7 +167,8 @@ services:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: ${PULL_POLICY:-always}
pull_policy: if_not_present
restart: unless-stopped
ports:
- ${NGINX_PORT}:80
depends_on:
+249 -131
View File
@@ -1,18 +1,18 @@
#!/bin/bash
BRANCH=master
BRANCH=${BRANCH:-master}
SCRIPT_DIR=$PWD
SERVICE_FOLDER=plane-app
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE=$BRANCH
export APP_RELEASE="stable"
export DOCKERHUB_USER=makeplane
export PULL_POLICY=always
USE_GLOBAL_IMAGES=1
export PULL_POLICY=${PULL_POLICY:-if_not_present}
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
CPU_ARCH=$(uname -m)
mkdir -p $PLANE_INSTALL_DIR/archive
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
function print_header() {
clear
@@ -31,65 +31,191 @@ Project management tool from the future
EOF
}
function buildLocalImage() {
if [ "$1" == "--force-build" ]; then
DO_BUILD="1"
elif [ "$1" == "--skip-build" ]; then
DO_BUILD="2"
else
printf "\n" >&2
printf "${YELLOW}You are on ${CPU_ARCH} cpu architecture. ${NC}\n" >&2
printf "${YELLOW}Since the prebuilt ${CPU_ARCH} compatible docker images are not available for, we will be running the docker build on this system. ${NC} \n" >&2
printf "${YELLOW}This might take ${YELLOW}5-30 min based on your system's hardware configuration. \n ${NC} \n" >&2
printf "\n" >&2
printf "${GREEN}Select an option to proceed: ${NC}\n" >&2
printf " 1) Build Fresh Images \n" >&2
printf " 2) Skip Building Images \n" >&2
printf " 3) Exit \n" >&2
printf "\n" >&2
read -p "Select Option [1]: " DO_BUILD
until [[ -z "$DO_BUILD" || "$DO_BUILD" =~ ^[1-3]$ ]]; do
echo "$DO_BUILD: invalid selection." >&2
read -p "Select Option [1]: " DO_BUILD
done
function spinner() {
local pid=$1
local delay=.5
local spinstr='|/-\'
if ! ps -p "$pid" > /dev/null; then
echo "Invalid PID: $pid"
return 1
fi
while ps -p "$pid" > /dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr" >&2
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b" >&2
done
printf " \b\b\b\b" >&2
}
function initialize(){
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${CPU_ARCH^^} support." >&2
if [ "$CUSTOM_BUILD" == "true" ]; then
echo "" >&2
echo "" >&2
echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2
echo "build"
return 1
fi
if [ "$DO_BUILD" == "1" ] || [ "$DO_BUILD" == "" ];
then
REPO=https://github.com/makeplane/plane.git
CURR_DIR=$PWD
PLANE_TEMP_CODE_DIR=$(mktemp -d)
git clone $REPO $PLANE_TEMP_CODE_DIR --branch $BRANCH --single-branch
local IMAGE_NAME=makeplane/plane-proxy
local IMAGE_TAG=${APP_RELEASE}
docker manifest inspect "${IMAGE_NAME}:${IMAGE_TAG}" | grep -q "\"architecture\": \"${CPU_ARCH}\"" &
local pid=$!
spinner "$pid"
echo "" >&2
cp $PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml $PLANE_TEMP_CODE_DIR/build.yml
wait "$pid"
cd $PLANE_TEMP_CODE_DIR
if [ "$BRANCH" == "master" ];
then
export APP_RELEASE=stable
fi
/bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
# cd $CURR_DIR
# rm -rf $PLANE_TEMP_CODE_DIR
echo "build_completed"
elif [ "$DO_BUILD" == "2" ];
then
printf "${YELLOW}Build action skipped by you in lieu of using existing images. ${NC} \n" >&2
echo "build_skipped"
elif [ "$DO_BUILD" == "3" ];
then
echo "build_exited"
if [ $? -eq 0 ]; then
echo "Plane supports ${CPU_ARCH}" >&2
echo "available"
return 0
else
printf "INVALID OPTION SUPPLIED" >&2
echo "" >&2
echo "" >&2
echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2
echo "" >&2
echo "build"
return 1
fi
}
function install() {
echo "Installing Plane.........."
download
function getEnvValue() {
local key=$1
local file=$2
if [ -z "$key" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
grep -q "^$key=" "$file"
if [ $? -eq 0 ]; then
local value
value=$(grep "^$key=" "$file" | cut -d'=' -f2)
echo "$value"
else
echo ""
fi
fi
}
function updateEnvFile() {
local key=$1
local value=$2
local file=$3
if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
# check if key exists in the file
grep -q "^$key=" "$file"
if [ $? -ne 0 ]; then
echo "$key=$value" >> "$file"
return
else
# if key exists, update the value
sed -i "s/^$key=.*/$key=$value/g" "$file"
fi
else
echo "File not found: $file"
exit 1
fi
}
function updateCustomVariables(){
echo "Updating custom variables..." >&2
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
echo "Custom variables updated successfully" >&2
}
function syncEnvFile(){
echo "Syncing environment variables..." >&2
if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then
updateCustomVariables
# READ keys of plane.env and update the values from plane.env.bak
while IFS= read -r line
do
# ignore is the line is empty or starts with #
if [ -z "$line" ] || [[ $line == \#* ]]; then
continue
fi
key=$(echo "$line" | cut -d'=' -f1)
value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak")
if [ -n "$value" ]; then
updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH"
fi
done < "$DOCKER_ENV_PATH"
fi
echo "Environment variables synced successfully" >&2
}
function buildYourOwnImage(){
echo "Building images locally..."
export DOCKERHUB_USER="myplane"
export APP_RELEASE="local"
export PULL_POLICY="never"
CUSTOM_BUILD="true"
# checkout the code to ~/tmp/plane folder and build the images
local PLANE_TEMP_CODE_DIR=~/tmp/plane
rm -rf $PLANE_TEMP_CODE_DIR
mkdir -p $PLANE_TEMP_CODE_DIR
REPO=https://github.com/makeplane/plane.git
git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1
cp "$PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml"
cd "$PLANE_TEMP_CODE_DIR" || exit
/bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
if [ $? -ne 0 ]; then
echo "Build failed. Exiting..."
exit 1
fi
echo "Build completed successfully"
echo ""
echo "You can now start the services by running the command: ./setup.sh start"
echo ""
}
function install() {
echo "Begin Installing Plane"
echo ""
local build_image=$(initialize)
if [ "$build_image" == "build" ]; then
# ask for confirmation to continue building the images
echo "Do you want to continue with building the Docker images locally?"
read -p "Continue? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Exiting..."
exit 0
fi
fi
if [ "$build_image" == "build" ]; then
download "true"
else
download "false"
fi
}
function download() {
local LOCAL_BUILD=$1
cd $SCRIPT_DIR
TS=$(date +%s)
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
@@ -102,44 +228,48 @@ function download() {
if [ -f "$DOCKER_ENV_PATH" ];
then
cp $DOCKER_ENV_PATH $PLANE_INSTALL_DIR/archive/$TS.env
else
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
fi
if [ "$BRANCH" != "master" ];
then
cp $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/temp.yaml
sed -e 's@${APP_RELEASE:-stable}@'"$BRANCH"'@g' \
$PLANE_INSTALL_DIR/temp.yaml > $PLANE_INSTALL_DIR/docker-compose.yaml
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
rm $PLANE_INSTALL_DIR/temp.yaml
fi
syncEnvFile
if [ $USE_GLOBAL_IMAGES == 0 ]; then
local res=$(buildLocalImage)
# echo $res
if [ "$LOCAL_BUILD" == "true" ]; then
export DOCKERHUB_USER="myplane"
export APP_RELEASE="local"
export PULL_POLICY="never"
CUSTOM_BUILD="true"
if [ "$res" == "build_exited" ];
then
echo
echo "Install action cancelled by you. Exiting now."
echo
exit 0
buildYourOwnImage
if [ $? -ne 0 ]; then
echo ""
echo "Build failed. Exiting..."
exit 1
fi
updateCustomVariables
else
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull"
CUSTOM_BUILD="false"
updateCustomVariables
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull --policy always"
if [ $? -ne 0 ]; then
echo ""
echo "Failed to pull the images. Exiting..."
exit 1
fi
fi
echo ""
echo "Most recent Stable version is now available for you to use"
echo "Most recent version of Plane is now available for you to use"
echo ""
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in 'plane.env 'file."
echo "In case of 'Upgrade', please check the 'plane.env 'file for any new variables and update them accordingly"
echo ""
}
function startServices() {
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --quiet-pull"
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --pull if_not_present --quiet-pull"
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
if [ -n "$migrator_container_id" ]; then
@@ -201,7 +331,7 @@ function upgrade() {
echo
echo "***** DOWNLOADING STABLE VERSION ****"
download
install
echo "***** PLEASE VALIDATE AND START SERVICES ****"
}
@@ -282,7 +412,6 @@ function viewLogs(){
echo "INVALID SERVICE NAME SUPPLIED"
fi
}
function backupSingleVolume() {
backupFolder=$1
selectedVolume=$2
@@ -299,7 +428,6 @@ function backupSingleVolume() {
-v "$backupFolder":/backup \
busybox sh -c 'tar -czf "/backup/${TAR_NAME}.tar.gz" /${TAR_NAME}'
}
function backupData() {
local datetime=$(date +"%Y%m%d-%H%M")
local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime
@@ -329,7 +457,7 @@ function askForAction() {
then
echo
echo "Select a Action you want to perform:"
echo " 1) Install (${CPU_ARCH})"
echo " 1) Install"
echo " 2) Start"
echo " 3) Stop"
echo " 4) Restart"
@@ -351,31 +479,31 @@ function askForAction() {
echo
fi
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ]
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ];
then
install
askForAction
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ]
# askForAction
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ];
then
startServices
# askForAction
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ]
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ];
then
stopServices
# askForAction
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ]
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ];
then
restartServices
# askForAction
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ]
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ];
then
upgrade
askForAction
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ]
# askForAction
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ];
then
viewLogs $@
viewLogs "$@"
askForAction
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ]
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ];
then
backupData
elif [ "$ACTION" == "8" ]
@@ -394,48 +522,38 @@ else
COMPOSE_CMD="docker compose"
fi
# CPU ARCHITECHTURE BASED SETTINGS
CPU_ARCH=$(uname -m)
if [[ $FORCE_CPU == "amd64" || $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]];
then
USE_GLOBAL_IMAGES=1
DOCKERHUB_USER=makeplane
PULL_POLICY=always
else
USE_GLOBAL_IMAGES=0
DOCKERHUB_USER=myplane
PULL_POLICY=never
if [ "$CPU_ARCH" == "x86_64" ] || [ "$CPU_ARCH" == "amd64" ]; then
CPU_ARCH="amd64"
elif [ "$CPU_ARCH" == "aarch64" ] || [ "$CPU_ARCH" == "arm64" ]; then
CPU_ARCH="arm64"
fi
if [ "$BRANCH" == "master" ];
then
export APP_RELEASE=stable
fi
if [ -f "$DOCKER_ENV_PATH" ]; then
DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH")
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
PULL_POLICY=$(getEnvValue "PULL_POLICY" "$DOCKER_ENV_PATH")
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
# REMOVE SPECIAL CHARACTERS FROM BRANCH NAME
if [ "$BRANCH" != "master" ];
then
SERVICE_FOLDER=plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g')
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
fi
mkdir -p $PLANE_INSTALL_DIR/archive
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
if [ -z "$APP_RELEASE" ]; then
APP_RELEASE=stable
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
fi
# BACKWARD COMPATIBILITY
OLD_DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/.env
if [ -f "$OLD_DOCKER_ENV_PATH" ];
then
mv "$OLD_DOCKER_ENV_PATH" "$DOCKER_ENV_PATH"
OS_NAME=$(uname)
if [ "$OS_NAME" == "Darwin" ];
then
sed -i '' -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
else
sed -i -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
if [ -z "$PULL_POLICY" ]; then
PULL_POLICY=if_not_present
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
fi
if [ -z "$CUSTOM_BUILD" ]; then
CUSTOM_BUILD=false
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
fi
fi
print_header
askForAction $@
askForAction "$@"
+5 -3
View File
@@ -1,3 +1,4 @@
APP_DOMAIN=localhost
APP_RELEASE=stable
WEB_REPLICAS=1
@@ -6,11 +7,11 @@ ADMIN_REPLICAS=1
API_REPLICAS=1
NGINX_PORT=80
WEB_URL=http://localhost
WEB_URL=http://${APP_DOMAIN}
DEBUG=0
SENTRY_DSN=
SENTRY_ENVIRONMENT=production
CORS_ALLOWED_ORIGINS=http://localhost
CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
#DB SETTINGS
PGHOST=plane-db
@@ -46,4 +47,5 @@ FILE_SIZE_LIMIT=5242880
GUNICORN_WORKERS=1
# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
# DOCKER_PLATFORM=linux/amd64
# DOCKER_PLATFORM=linux/amd64
@@ -17,6 +17,7 @@ export const EnterKeyExtension = (onEnterKeyPress?: (descriptionHTML: string) =>
editor.commands.first(({ commands }) => [
() => commands.newlineInCode(),
() => commands.splitListItem("listItem"),
() => commands.splitListItem("taskItem"),
() => commands.createParagraphNear(),
() => commands.liftEmptyBlock(),
() => commands.splitBlock(),
@@ -30,7 +30,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
return (
<>
<Moveable
target={document.querySelector(".ProseMirror-selectednode") as HTMLElement}
target={document.querySelector(".active-editor .ProseMirror-selectednode") as HTMLElement}
container={null}
origin={false}
edge={false}
@@ -39,7 +39,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
resizable
throttleResize={0}
onResizeStart={() => {
const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
const imageInfo = document.querySelector(".active-editor .ProseMirror-selectednode") as HTMLImageElement;
if (imageInfo) {
const originalWidth = Number(imageInfo.width);
const originalHeight = Number(imageInfo.height);
+1 -1
View File
@@ -369,7 +369,7 @@ const ProfileSettingsPage = observer(() => {
/>
)}
/>
{errors?.display_name && <span className="text-xs text-red-500">Please enter display name</span>}
{errors?.display_name && <span className="text-xs text-red-500">{errors?.display_name?.message}</span>}
</div>
<div className="flex flex-col gap-1">
@@ -66,7 +66,6 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
projectId={projectId}
workspaceSlug={workspaceSlug}
onEnterKeyPress={(commentHTML) => {
console.log("commentHTML", commentHTML);
const isEmpty =
commentHTML?.trim() === "" ||
commentHTML === "<p></p>" ||
@@ -1,6 +1,7 @@
"use client";
import { FC, useEffect, Fragment } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
import type { TIssueLinkEditableFields } from "@plane/types";
@@ -27,7 +28,7 @@ const defaultValues: TIssueLinkCreateFormFieldOptions = {
url: "",
};
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props) => {
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observer((props) => {
// props
const { isModalOpen, handleOnClose, linkOperations } = props;
@@ -45,6 +46,7 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
const onClose = () => {
setIssueLinkData(null);
reset(defaultValues);
if (handleOnClose) handleOnClose();
};
@@ -55,8 +57,8 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
};
useEffect(() => {
reset({ ...defaultValues, ...preloadedData });
}, [preloadedData, reset]);
if (isModalOpen) reset({ ...defaultValues, ...preloadedData });
}, [preloadedData, reset, isModalOpen]);
return (
<Transition.Root show={isModalOpen} as={Fragment}>
@@ -165,4 +167,4 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
</Dialog>
</Transition.Root>
);
};
});
@@ -112,7 +112,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
</div>
<div
className={`relative flex items-center gap-1 ${
className={`relative flex items-baseline gap-1 ${
verticalAlignPosition ? `flex-col` : `w-full flex-row overflow-hidden`
}`}
>
@@ -122,7 +122,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
<DropIndicator classNames={"absolute top-0 z-[2]"} isVisible={instruction === "DRAG_OVER"} />
<RenderIfVisible
key={`${issueId}`}
defaultHeight="inherit"
defaultHeight="3rem"
root={containerRef}
classNames={`relative ${isLastChild && !isExpanded ? "" : "border-b border-b-custom-border-200"}`}
verticalOffset={100}
@@ -1,3 +1,4 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { Trash2 } from "lucide-react";
@@ -6,7 +7,7 @@ import { IUser, IWorkspaceMember } from "@plane/types";
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
import { EUserProjectRoles } from "@/constants/project";
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
import { useMember } from "@/hooks/store";
import { useMember, useUser } from "@/hooks/store";
export interface RowData {
member: IWorkspaceMember;
@@ -80,62 +81,74 @@ export const NameColumn: React.FC<NameProps> = (props) => {
);
};
export const AccountTypeColumn: React.FC<AccountTypeProps> = (props) => {
export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) => {
const { rowData, currentProjectRole, projectId, workspaceSlug } = props;
// form info
const {
control,
formState: { errors },
} = useForm();
// store hooks
const {
project: { updateMember },
} = useMember();
return rowData.role === EUserWorkspaceRoles.ADMIN || currentProjectRole !== EUserProjectRoles.ADMIN ? (
<div className="w-32 flex ">
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
</div>
) : (
<Controller
name="role"
control={control}
rules={{ required: "Role is required." }}
render={({ field: { value } }) => (
<CustomSelect
value={value}
onChange={(value: EUserProjectRoles) => {
if (!workspaceSlug) return;
const { data: currentUser } = useUser();
updateMember(workspaceSlug.toString(), projectId.toString(), rowData.member.id, {
role: value as unknown as EUserProjectRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
}).catch((err) => {
console.log(err, "err");
const error = err.error;
const errorString = Array.isArray(error) ? error[0] : error;
// derived values
const isCurrentUser = currentUser?.id === rowData.member.id;
const isAdminRole = currentProjectRole === EUserProjectRoles.ADMIN;
const isRoleEditable = isCurrentUser && isAdminRole;
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
});
}}
label={
<div className="flex ">
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
</div>
}
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
className="rounded-md p-0 w-32"
optionsClassName="w-full"
input
>
{Object.keys(ROLE).map((item) => (
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
{ROLE[item as unknown as keyof typeof ROLE]}
</CustomSelect.Option>
))}
</CustomSelect>
return (
<>
{isRoleEditable ? (
<div className="w-32 flex ">
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
</div>
) : (
<Controller
name="role"
control={control}
rules={{ required: "Role is required." }}
render={({ field: { value } }) => (
<CustomSelect
value={value}
onChange={(value: EUserProjectRoles) => {
if (!workspaceSlug) return;
updateMember(workspaceSlug.toString(), projectId.toString(), rowData.member.id, {
role: value as unknown as EUserProjectRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
}).catch((err) => {
console.log(err, "err");
const error = err.error;
const errorString = Array.isArray(error) ? error[0] : error;
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
});
}}
label={
<div className="flex ">
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
</div>
}
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
className="rounded-md p-0 w-32"
optionsClassName="w-full"
input
>
{Object.keys(ROLE).map((item) => (
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
{ROLE[item as unknown as keyof typeof ROLE]}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
)}
/>
</>
);
};
});
@@ -11,11 +11,10 @@ export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { ca
{cardsInEachColumn.map((cardsInColumn, columnIndex) => (
<div key={columnIndex} className="flex flex-col gap-3">
<div className="flex items-center justify-between h-9 w-80">
<div className="flex item-center">
<div className="flex item-center gap-3 px-1.5">
<span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
<span className="h-6 w-24 bg-custom-background-80 rounded animate-pulse" />
</div>
<span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
</div>
{Array.from({ length: cardsInColumn }, (_, cardIndex) => (
<KanbanIssueBlockLoader key={cardIndex} />
@@ -31,12 +31,12 @@ export const NotificationFilterOptionItem: FC<{ label: string; value: ENotificat
onClick={() => handleFilterTypeChange(value, !isSelected)}
>
<div
className={cn(
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all",
isSelected ? "bg-custom-primary-100" : "bg-custom-background-90"
)}
className={cn("flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all", {
"bg-custom-primary text-white": isSelected,
"bg-custom-background-90": !isSelected,
})}
>
{isSelected && <Check className="h-2 w-2" />}
{isSelected && <Check className="h-2.5 w-2.5" />}
</div>
<div className={cn("whitespace-nowrap text-sm", isSelected ? "text-custom-text-100" : "text-custom-text-200")}>
{label}
@@ -1,3 +1,4 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { Trash2 } from "lucide-react";
@@ -6,7 +7,7 @@ import { IUser, IWorkspaceMember } from "@plane/types";
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
import { EUserProjectRoles } from "@/constants/project";
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
import { useMember } from "@/hooks/store";
import { useMember, useUser } from "@/hooks/store";
export interface RowData {
member: IWorkspaceMember;
@@ -79,63 +80,75 @@ export const NameColumn: React.FC<NameProps> = (props) => {
);
};
export const AccountTypeColumn: React.FC<AccountTypeProps> = (props) => {
export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) => {
const { rowData, currentWorkspaceRole, workspaceSlug } = props;
// form info
const {
control,
formState: { errors },
} = useForm();
// store hooks
const {
workspace: { updateMember },
} = useMember();
return rowData.role === EUserWorkspaceRoles.ADMIN || currentWorkspaceRole !== EUserWorkspaceRoles.ADMIN ? (
<div className="w-32 flex ">
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
</div>
) : (
<Controller
name="role"
control={control}
rules={{ required: "Role is required." }}
render={({ field: { value } }) => (
<CustomSelect
value={value}
onChange={(value: EUserProjectRoles) => {
console.log({ value, workspaceSlug }, "onChange");
if (!workspaceSlug) return;
const { data: currentUser } = useUser();
updateMember(workspaceSlug.toString(), rowData.member.id, {
role: value as unknown as EUserWorkspaceRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
}).catch((err) => {
console.log(err, "err");
const error = err.error;
const errorString = Array.isArray(error) ? error[0] : error;
// derived values
const isCurrentUser = currentUser?.id === rowData.member.id;
const isAdminRole = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
const isRoleEditable = isCurrentUser && isAdminRole;
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
});
}}
label={
<div className="flex ">
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
</div>
}
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
className="rounded-md p-0 w-32"
optionsClassName="w-full"
input
>
{Object.keys(ROLE).map((item) => (
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
{ROLE[item as unknown as keyof typeof ROLE]}
</CustomSelect.Option>
))}
</CustomSelect>
return (
<>
{isRoleEditable ? (
<div className="w-32 flex ">
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
</div>
) : (
<Controller
name="role"
control={control}
rules={{ required: "Role is required." }}
render={({ field: { value } }) => (
<CustomSelect
value={value}
onChange={(value: EUserProjectRoles) => {
console.log({ value, workspaceSlug }, "onChange");
if (!workspaceSlug) return;
updateMember(workspaceSlug.toString(), rowData.member.id, {
role: value as unknown as EUserWorkspaceRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
}).catch((err) => {
console.log(err, "err");
const error = err.error;
const errorString = Array.isArray(error) ? error[0] : error;
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
});
}}
label={
<div className="flex ">
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
</div>
}
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
className="rounded-md p-0 w-32"
optionsClassName="w-full"
input
>
{Object.keys(ROLE).map((item) => (
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
{ROLE[item as unknown as keyof typeof ROLE]}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
)}
/>
</>
);
};
});
+6 -1
View File
@@ -119,7 +119,12 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.getAppliedFilters(cycleId);
let filterParams = this.getAppliedFilters(cycleId);
if (!filterParams) {
filterParams = {};
}
filterParams["cycle"] = cycleId;
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
return paginationParams;
+2 -2
View File
@@ -164,7 +164,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
// get params from pagination options
const params = this.issueFilterStore?.getFilterParams(options, cycleId, undefined, undefined, undefined);
// call the fetch issues API with the params
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params, {
const response = await this.issueService.getIssues(workspaceSlug, projectId, params, {
signal: this.controller.signal,
});
@@ -212,7 +212,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
subGroupId
);
// call the fetch issues API with the params for next page in issues
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
const response = await this.issueService.getIssues(workspaceSlug, projectId, cycleId, params);
// after the next page of issues are fetched, call the base method to process the response
this.onfetchNexIssues(response, groupId, subGroupId);
@@ -892,9 +892,14 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
if (issueCycleId === cycleId) return;
try {
// Update issueIds from current store
runInAction(() => {
// If cycle Id before update is the same as current cycle Id then, remove issueId from list
if (this.cycleId === issueCycleId) this.removeIssueFromList(issueId);
// If cycle Id is the current cycle Id, then, add issue to list of issueIds
if (this.cycleId === cycleId) this.addIssueToList(issueId);
// For Each issue update cycle Id by calling current store's update Issue, without making an API call
+6 -1
View File
@@ -119,7 +119,12 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.getAppliedFilters(moduleId);
let filterParams = this.getAppliedFilters(moduleId);
if (!filterParams) {
filterParams = {};
}
filterParams["module"] = moduleId;
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
return paginationParams;
+2 -2
View File
@@ -118,7 +118,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
// get params from pagination options
const params = this.issueFilterStore?.getFilterParams(options, moduleId, undefined, undefined, undefined);
// call the fetch issues API with the params
const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params, {
const response = await this.issueService.getIssues(workspaceSlug, projectId, params, {
signal: this.controller.signal,
});
@@ -166,7 +166,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
subGroupId
);
// call the fetch issues API with the params for next page in issues
const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
// after the next page of issues are fetched, call the base method to process the response
this.onfetchNexIssues(response, groupId, subGroupId);