Compare commits

...

11 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
29 changed files with 830 additions and 125 deletions
+2
View File
@@ -166,6 +166,7 @@ class IssueListEndpoint(BaseAPIView):
"link_count",
"is_draft",
"archived_at",
"deleted_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
@@ -400,6 +401,7 @@ class IssueViewSet(BaseViewSet):
"link_count",
"is_draft",
"archived_at",
"deleted_at",
)
.first()
)
@@ -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'),
),
]
@@ -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`
}`}
>
@@ -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);