From 6a09e51541982a77a26af5d7274a798840263bb0 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 1 Dec 2025 14:47:59 +0530 Subject: [PATCH 01/14] refactor: rename IssueUserProperty to ProjectUserProperty and update related references across the codebase --- apps/api/plane/api/views/project.py | 6 +-- apps/api/plane/app/serializers/issue.py | 4 +- apps/api/plane/app/urls/issue.py | 4 +- apps/api/plane/app/views/issue/base.py | 6 +-- apps/api/plane/app/views/project/base.py | 6 +-- apps/api/plane/app/views/project/invite.py | 6 +-- apps/api/plane/app/views/project/member.py | 6 +-- apps/api/plane/bgtasks/workspace_seed_task.py | 6 +-- .../commands/create_project_member.py | 4 +- .../0113_alter_issueuserproperty_table.py | 38 +++++++++++++++++ .../db/migrations/0114_auto_20251201_0857.py | 42 +++++++++++++++++++ apps/api/plane/db/models/__init__.py | 2 +- apps/api/plane/db/models/api.py | 1 + apps/api/plane/db/models/issue.py | 30 ------------- apps/api/plane/db/models/project.py | 35 +++++++++++++++- .../tests/contract/app/test_project_app.py | 10 ++--- 16 files changed, 145 insertions(+), 61 deletions(-) create mode 100644 apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py create mode 100644 apps/api/plane/db/migrations/0114_auto_20251201_0857.py diff --git a/apps/api/plane/api/views/project.py b/apps/api/plane/api/views/project.py index fa735f557d7..698343ded16 100644 --- a/apps/api/plane/api/views/project.py +++ b/apps/api/plane/api/views/project.py @@ -18,7 +18,7 @@ from plane.db.models import ( Cycle, Intake, - IssueUserProperty, + ProjectUserProperty, Module, Project, DeployBoard, @@ -217,7 +217,7 @@ def post(self, request, slug): # Add the user as Administrator to the project _ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20) # Also create the issue property for the user - _ = IssueUserProperty.objects.create(project_id=serializer.instance.id, user=request.user) + _ = ProjectUserProperty.objects.create(project_id=serializer.instance.id, user=request.user) if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str( request.user.id @@ -228,7 +228,7 @@ def post(self, request, slug): role=20, ) # Also create the issue property for the user - IssueUserProperty.objects.create( + ProjectUserProperty.objects.create( project_id=serializer.instance.id, user_id=serializer.instance.project_lead, ) diff --git a/apps/api/plane/app/serializers/issue.py b/apps/api/plane/app/serializers/issue.py index 5e3b93ab674..c71bd8447f6 100644 --- a/apps/api/plane/app/serializers/issue.py +++ b/apps/api/plane/app/serializers/issue.py @@ -18,7 +18,7 @@ Issue, IssueActivity, IssueComment, - IssueUserProperty, + ProjectUserProperty, IssueAssignee, IssueSubscriber, IssueLabel, @@ -348,7 +348,7 @@ class Meta: class IssueUserPropertySerializer(BaseSerializer): class Meta: - model = IssueUserProperty + model = ProjectUserProperty fields = "__all__" read_only_fields = ["user", "workspace", "project"] diff --git a/apps/api/plane/app/urls/issue.py b/apps/api/plane/app/urls/issue.py index 1d809e248f5..e39ce9f9c55 100644 --- a/apps/api/plane/app/urls/issue.py +++ b/apps/api/plane/app/urls/issue.py @@ -208,13 +208,13 @@ name="project-issue-comment-reactions", ), ## End Comment Reactions - ## IssueUserProperty + ## ProjectUserProperty path( "workspaces//projects//user-properties/", IssueUserDisplayPropertyEndpoint.as_view(), name="project-issue-display-properties", ), - ## IssueUserProperty End + ## ProjectUserProperty End ## Issue Archives path( "workspaces//projects//archived-issues/", diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index c24db616980..9dace60e760 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -51,7 +51,7 @@ IssueReaction, IssueRelation, IssueSubscriber, - IssueUserProperty, + ProjectUserProperty, ModuleIssue, Project, ProjectMember, @@ -718,7 +718,7 @@ def destroy(self, request, slug, project_id, pk=None): class IssueUserDisplayPropertyEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id): - issue_property = IssueUserProperty.objects.get(user=request.user, project_id=project_id) + issue_property = ProjectUserProperty.objects.get(user=request.user, project_id=project_id) issue_property.rich_filters = request.data.get("rich_filters", issue_property.rich_filters) issue_property.filters = request.data.get("filters", issue_property.filters) @@ -730,7 +730,7 @@ def patch(self, request, slug, project_id): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id): - issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id) + issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id) serializer = IssueUserPropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index 3aa35649167..f98fc684c1b 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -26,7 +26,7 @@ UserFavorite, Intake, DeployBoard, - IssueUserProperty, + ProjectUserProperty, Project, ProjectIdentifier, ProjectMember, @@ -249,7 +249,7 @@ def create(self, request, slug): role=ROLE.ADMIN.value, ) # Also create the issue property for the user - _ = IssueUserProperty.objects.create(project_id=serializer.data["id"], user=request.user) + _ = ProjectUserProperty.objects.create(project_id=serializer.data["id"], user=request.user) if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str( request.user.id @@ -260,7 +260,7 @@ def create(self, request, slug): role=ROLE.ADMIN.value, ) # Also create the issue property for the user - IssueUserProperty.objects.create( + ProjectUserProperty.objects.create( project_id=serializer.data["id"], user_id=serializer.data["project_lead"], ) diff --git a/apps/api/plane/app/views/project/invite.py b/apps/api/plane/app/views/project/invite.py index cc5b3f4b577..6ae5a5e11a5 100644 --- a/apps/api/plane/app/views/project/invite.py +++ b/apps/api/plane/app/views/project/invite.py @@ -24,7 +24,7 @@ User, WorkspaceMember, Project, - IssueUserProperty, + ProjectUserProperty, ) from plane.db.models.project import ProjectNetwork from plane.utils.host import base_host @@ -160,9 +160,9 @@ def create(self, request, slug): ignore_conflicts=True, ) - IssueUserProperty.objects.bulk_create( + ProjectUserProperty.objects.bulk_create( [ - IssueUserProperty( + ProjectUserProperty( project_id=project_id, user=request.user, workspace=workspace, diff --git a/apps/api/plane/app/views/project/member.py b/apps/api/plane/app/views/project/member.py index 69d45226ce8..664355a17a6 100644 --- a/apps/api/plane/app/views/project/member.py +++ b/apps/api/plane/app/views/project/member.py @@ -13,7 +13,7 @@ from plane.app.permissions import WorkspaceUserPermission -from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember +from plane.db.models import Project, ProjectMember, ProjectUserProperty, WorkspaceMember from plane.bgtasks.project_add_user_email_task import project_add_user_email from plane.utils.host import base_host from plane.app.permissions.base import allow_permission, ROLE @@ -119,7 +119,7 @@ def create(self, request, slug, project_id): ) # Create a new issue property bulk_issue_props.append( - IssueUserProperty( + ProjectUserProperty( user_id=member.get("member_id"), project_id=project_id, workspace_id=project.workspace_id, @@ -129,7 +129,7 @@ def create(self, request, slug, project_id): # Bulk create the project members and issue properties project_members = ProjectMember.objects.bulk_create(bulk_project_members, batch_size=10, ignore_conflicts=True) - _ = IssueUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True) + _ = ProjectUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True) project_members = ProjectMember.objects.filter( project_id=project_id, diff --git a/apps/api/plane/bgtasks/workspace_seed_task.py b/apps/api/plane/bgtasks/workspace_seed_task.py index fb9980c3fd6..6565ed64667 100644 --- a/apps/api/plane/bgtasks/workspace_seed_task.py +++ b/apps/api/plane/bgtasks/workspace_seed_task.py @@ -20,7 +20,7 @@ WorkspaceMember, Project, ProjectMember, - IssueUserProperty, + ProjectUserProperty, State, Label, Issue, @@ -118,9 +118,9 @@ def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]: ) # Create issue user properties - IssueUserProperty.objects.bulk_create( + ProjectUserProperty.objects.bulk_create( [ - IssueUserProperty( + ProjectUserProperty( project=project, user_id=workspace_member["member_id"], workspace_id=workspace.id, diff --git a/apps/api/plane/db/management/commands/create_project_member.py b/apps/api/plane/db/management/commands/create_project_member.py index d9b46524c28..a097056b99b 100644 --- a/apps/api/plane/db/management/commands/create_project_member.py +++ b/apps/api/plane/db/management/commands/create_project_member.py @@ -8,7 +8,7 @@ WorkspaceMember, ProjectMember, Project, - IssueUserProperty, + ProjectUserProperty, ) @@ -67,7 +67,7 @@ def handle(self, *args: Any, **options: Any): ProjectMember.objects.create(project=project, member=user, role=role, sort_order=sort_order) # Issue Property - IssueUserProperty.objects.get_or_create(user=user, project=project) + ProjectUserProperty.objects.get_or_create(user=user, project=project) # Success message self.stdout.write(self.style.SUCCESS(f"User {user_email} added to project {project_id}")) diff --git a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py new file mode 100644 index 00000000000..3083f96654d --- /dev/null +++ b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.25 on 2025-11-28 14:36 + +from django.db import migrations, models +import plane.db.models.project + + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0112_auto_20251124_0603'), + ] + + operations = [ + migrations.AlterModelTable( + name='issueuserproperty', + table='project_user_properties', + ), + migrations.RenameModel( + old_name='IssueUserProperty', + new_name='ProjectUserProperty', + ), + migrations.AddField( + model_name='apitoken', + name='allowed_rate_limit', + field=models.CharField(default='60/min', max_length=255), + ), + migrations.AddField( + model_name='projectuserproperty', + name='preferences', + field=models.JSONField(default=plane.db.models.project.get_default_preferences), + ), + migrations.AddField( + model_name='projectuserproperty', + name='sort_order', + field=models.FloatField(default=65535), + ), + ] diff --git a/apps/api/plane/db/migrations/0114_auto_20251201_0857.py b/apps/api/plane/db/migrations/0114_auto_20251201_0857.py new file mode 100644 index 00000000000..0ca5961d2dd --- /dev/null +++ b/apps/api/plane/db/migrations/0114_auto_20251201_0857.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.25 on 2025-12-01 08:57 + +from django.db import migrations + +def move_issue_user_properties_to_project_user_properties(apps, schema_editor): + ProjectMember = apps.get_model('db', 'ProjectMember') + ProjectUserProperty = apps.get_model('db', 'ProjectUserProperty') + + # Get all project members + project_members = ProjectMember.objects.filter(deleted_at__isnull=True).values('member_id', 'project_id', 'preferences', 'sort_order') + + # create a mapping with consistent ordering + pm_dict = { + (pm['member_id'], pm['project_id']): pm + for pm in project_members + } + + # Get all project user properties + properties_to_update = [] + for projectuserproperty in ProjectUserProperty.objects.filter(deleted_at__isnull=True): + pm = pm_dict.get((projectuserproperty.user_id, projectuserproperty.project_id)) + if pm: + projectuserproperty.preferences = pm['preferences'] + projectuserproperty.sort_order = pm['sort_order'] + properties_to_update.append(projectuserproperty) + + ProjectUserProperty.objects.bulk_update(properties_to_update, ['preferences', 'sort_order'], batch_size=2000) + + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0113_alter_issueuserproperty_table'), + ] + + operations = [ + migrations.RunPython( + move_issue_user_properties_to_project_user_properties, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/apps/api/plane/db/models/__init__.py b/apps/api/plane/db/models/__init__.py index d24a145644b..e668e2556ba 100644 --- a/apps/api/plane/db/models/__init__.py +++ b/apps/api/plane/db/models/__init__.py @@ -34,7 +34,6 @@ IssueLabel, IssueLink, IssueMention, - IssueUserProperty, IssueReaction, IssueRelation, IssueSequence, @@ -53,6 +52,7 @@ ProjectMember, ProjectMemberInvite, ProjectPublicMember, + ProjectUserProperty, ) from .session import Session from .social_connection import SocialLoginConnection diff --git a/apps/api/plane/db/models/api.py b/apps/api/plane/db/models/api.py index 7d040ebc284..75449a74283 100644 --- a/apps/api/plane/db/models/api.py +++ b/apps/api/plane/db/models/api.py @@ -32,6 +32,7 @@ class APIToken(BaseModel): workspace = models.ForeignKey("db.Workspace", related_name="api_tokens", on_delete=models.CASCADE, null=True) expired_at = models.DateTimeField(blank=True, null=True) is_service = models.BooleanField(default=False) + allowed_rate_limit = models.CharField(max_length=255, default="60/min") class Meta: verbose_name = "API Token" diff --git a/apps/api/plane/db/models/issue.py b/apps/api/plane/db/models/issue.py index d3377f0ad37..277b4a174af 100644 --- a/apps/api/plane/db/models/issue.py +++ b/apps/api/plane/db/models/issue.py @@ -536,36 +536,6 @@ def __str__(self): return str(self.issue) -class IssueUserProperty(ProjectBaseModel): - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name="issue_property_user", - ) - filters = models.JSONField(default=get_default_filters) - display_filters = models.JSONField(default=get_default_display_filters) - display_properties = models.JSONField(default=get_default_display_properties) - rich_filters = models.JSONField(default=dict) - - class Meta: - verbose_name = "Issue User Property" - verbose_name_plural = "Issue User Properties" - db_table = "issue_user_properties" - ordering = ("-created_at",) - unique_together = ["user", "project", "deleted_at"] - constraints = [ - models.UniqueConstraint( - fields=["user", "project"], - condition=Q(deleted_at__isnull=True), - name="issue_user_property_unique_user_project_when_deleted_at_null", - ) - ] - - def __str__(self): - """Return properties status of the issue""" - return str(self.user) - - class IssueLabel(ProjectBaseModel): issue = models.ForeignKey("db.Issue", on_delete=models.CASCADE, related_name="label_issue") label = models.ForeignKey("db.Label", on_delete=models.CASCADE, related_name="label_issue") diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index 8495ac9df43..166fb55b020 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -12,7 +12,6 @@ # Module imports from plane.db.mixins import AuditModel -# Module imports from .base import BaseModel ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest")) @@ -313,3 +312,37 @@ class Meta: verbose_name_plural = "Project Public Members" db_table = "project_public_members" ordering = ("-created_at",) + + +class ProjectUserProperty(ProjectBaseModel): + + from .issue import get_default_filters, get_default_display_filters, get_default_display_properties + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="issue_property_user", + ) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) + rich_filters = models.JSONField(default=dict) + preferences = models.JSONField(default=get_default_preferences) + sort_order = models.FloatField(default=65535) + + class Meta: + verbose_name = "Issue User Property" + verbose_name_plural = "Issue User Properties" + db_table = "project_user_properties" + ordering = ("-created_at",) + unique_together = ["user", "project", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["user", "project"], + condition=Q(deleted_at__isnull=True), + name="issue_user_property_unique_user_project_when_deleted_at_null", + ) + ] + + def __str__(self): + """Return properties status of the issue""" + return str(self.user) diff --git a/apps/api/plane/tests/contract/app/test_project_app.py b/apps/api/plane/tests/contract/app/test_project_app.py index 38b0f51f3b5..9f05314cfee 100644 --- a/apps/api/plane/tests/contract/app/test_project_app.py +++ b/apps/api/plane/tests/contract/app/test_project_app.py @@ -6,7 +6,7 @@ from plane.db.models import ( Project, ProjectMember, - IssueUserProperty, + ProjectUserProperty, State, WorkspaceMember, User, @@ -82,8 +82,8 @@ def test_create_project_valid_data(self, session_client, workspace, create_user) assert project_member.role == 20 # Administrator assert project_member.is_active is True - # Verify IssueUserProperty was created - assert IssueUserProperty.objects.filter(project=project, user=user).exists() + # Verify ProjectUserProperty was created + assert ProjectUserProperty.objects.filter(project=project, user=user).exists() # Verify default states were created states = State.objects.filter(project=project) @@ -116,8 +116,8 @@ def test_create_project_with_project_lead(self, session_client, workspace, creat project = Project.objects.get(name=project_data["name"]) assert ProjectMember.objects.filter(project=project, role=20).count() == 2 - # Verify both have IssueUserProperty - assert IssueUserProperty.objects.filter(project=project).count() == 2 + # Verify both have ProjectUserProperty + assert ProjectUserProperty.objects.filter(project=project).count() == 2 @pytest.mark.django_db def test_create_project_guest_forbidden(self, session_client, workspace): From e43d9b65ed9bb5c0ad07beea93c6622af5dcaaee Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 1 Dec 2025 19:06:08 +0530 Subject: [PATCH 02/14] migrate: move issue user properties to project user properties and update related fields and constraints --- .../0113_alter_issueuserproperty_table.py | 20 +++++++++++++++++-- ...r_projectuserproperty_options_and_more.py} | 7 ++----- apps/api/plane/db/models/project.py | 10 +++++----- 3 files changed, 25 insertions(+), 12 deletions(-) rename apps/api/plane/db/migrations/{0114_auto_20251201_0857.py => 0114_alter_projectuserproperty_options_and_more.py} (86%) diff --git a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py index 3083f96654d..645633eb052 100644 --- a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py +++ b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py @@ -2,8 +2,8 @@ from django.db import migrations, models import plane.db.models.project - - +import django.db.models.deletion +from django.conf import settings class Migration(migrations.Migration): @@ -34,5 +34,21 @@ class Migration(migrations.Migration): model_name='projectuserproperty', name='sort_order', field=models.FloatField(default=65535), + ), migrations.AlterModelOptions( + name='projectuserproperty', + options={'ordering': ('-created_at',), 'verbose_name': 'Project User Property', 'verbose_name_plural': 'Project User Properties'}, + ), + migrations.RemoveConstraint( + model_name='projectuserproperty', + name='issue_user_property_unique_user_project_when_deleted_at_null', + ), + migrations.AlterField( + model_name='projectuserproperty', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_property_user', to=settings.AUTH_USER_MODEL), + ), + migrations.AddConstraint( + model_name='projectuserproperty', + constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('user', 'project'), name='project_user_property_unique_user_project_when_deleted_at_null'), ), ] diff --git a/apps/api/plane/db/migrations/0114_auto_20251201_0857.py b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py similarity index 86% rename from apps/api/plane/db/migrations/0114_auto_20251201_0857.py rename to apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py index 0ca5961d2dd..4fd35a93978 100644 --- a/apps/api/plane/db/migrations/0114_auto_20251201_0857.py +++ b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.25 on 2025-12-01 08:57 +# Generated by Django 4.2.25 on 2025-12-01 13:33 from django.db import migrations @@ -35,8 +35,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython( - move_issue_user_properties_to_project_user_properties, - reverse_code=migrations.RunPython.noop - ), + migrations.RunPython(move_issue_user_properties_to_project_user_properties, reverse_code=migrations.RunPython.noop), ] diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index 166fb55b020..580c8db2118 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -320,7 +320,7 @@ class ProjectUserProperty(ProjectBaseModel): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, - related_name="issue_property_user", + related_name="project_property_user", ) filters = models.JSONField(default=get_default_filters) display_filters = models.JSONField(default=get_default_display_filters) @@ -330,8 +330,8 @@ class ProjectUserProperty(ProjectBaseModel): sort_order = models.FloatField(default=65535) class Meta: - verbose_name = "Issue User Property" - verbose_name_plural = "Issue User Properties" + verbose_name = "Project User Property" + verbose_name_plural = "Project User Properties" db_table = "project_user_properties" ordering = ("-created_at",) unique_together = ["user", "project", "deleted_at"] @@ -339,10 +339,10 @@ class Meta: models.UniqueConstraint( fields=["user", "project"], condition=Q(deleted_at__isnull=True), - name="issue_user_property_unique_user_project_when_deleted_at_null", + name="project_user_property_unique_user_project_when_deleted_at_null", ) ] def __str__(self): - """Return properties status of the issue""" + """Return properties status of the project""" return str(self.user) From d874227de30d59ee6023b4b12dfa9dae295a27ac Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 1 Dec 2025 19:30:39 +0530 Subject: [PATCH 03/14] refactor: rename IssueUserPropertySerializer and IssueUserDisplayPropertyEndpoint to ProjectUserPropertySerializer and ProjectUserDisplayPropertyEndpoint, updating all related references --- apps/api/plane/app/serializers/__init__.py | 2 +- apps/api/plane/app/serializers/issue.py | 2 +- apps/api/plane/app/urls/issue.py | 4 ++-- apps/api/plane/app/views/__init__.py | 2 +- apps/api/plane/app/views/issue/base.py | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/api/plane/app/serializers/__init__.py b/apps/api/plane/app/serializers/__init__.py index 759f27ed6e6..96046187f9b 100644 --- a/apps/api/plane/app/serializers/__init__.py +++ b/apps/api/plane/app/serializers/__init__.py @@ -52,7 +52,7 @@ IssueCreateSerializer, IssueActivitySerializer, IssueCommentSerializer, - IssueUserPropertySerializer, + ProjectUserPropertySerializer, IssueAssigneeSerializer, LabelSerializer, IssueSerializer, diff --git a/apps/api/plane/app/serializers/issue.py b/apps/api/plane/app/serializers/issue.py index c71bd8447f6..c8f530ba18c 100644 --- a/apps/api/plane/app/serializers/issue.py +++ b/apps/api/plane/app/serializers/issue.py @@ -346,7 +346,7 @@ class Meta: fields = "__all__" -class IssueUserPropertySerializer(BaseSerializer): +class ProjectUserPropertySerializer(BaseSerializer): class Meta: model = ProjectUserProperty fields = "__all__" diff --git a/apps/api/plane/app/urls/issue.py b/apps/api/plane/app/urls/issue.py index e39ce9f9c55..1fd77412060 100644 --- a/apps/api/plane/app/urls/issue.py +++ b/apps/api/plane/app/urls/issue.py @@ -14,7 +14,7 @@ IssueReactionViewSet, IssueRelationViewSet, IssueSubscriberViewSet, - IssueUserDisplayPropertyEndpoint, + ProjectUserDisplayPropertyEndpoint, IssueViewSet, LabelViewSet, BulkArchiveIssuesEndpoint, @@ -211,7 +211,7 @@ ## ProjectUserProperty path( "workspaces//projects//user-properties/", - IssueUserDisplayPropertyEndpoint.as_view(), + ProjectUserDisplayPropertyEndpoint.as_view(), name="project-issue-display-properties", ), ## ProjectUserProperty End diff --git a/apps/api/plane/app/views/__init__.py b/apps/api/plane/app/views/__init__.py index 5f848a5ba03..6982b2bcfa2 100644 --- a/apps/api/plane/app/views/__init__.py +++ b/apps/api/plane/app/views/__init__.py @@ -115,7 +115,7 @@ from .issue.base import ( IssueListEndpoint, IssueViewSet, - IssueUserDisplayPropertyEndpoint, + ProjectUserDisplayPropertyEndpoint, BulkDeleteIssuesEndpoint, DeletedIssuesListViewSet, IssuePaginatedViewSet, diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index 9dace60e760..b938dc7ea7a 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -34,7 +34,7 @@ IssueDetailSerializer, IssueListDetailSerializer, IssueSerializer, - IssueUserPropertySerializer, + ProjectUserPropertySerializer, ) from plane.bgtasks.issue_activities_task import issue_activity from plane.bgtasks.issue_description_version_task import issue_description_version_task @@ -715,7 +715,7 @@ def destroy(self, request, slug, project_id, pk=None): return Response(status=status.HTTP_204_NO_CONTENT) -class IssueUserDisplayPropertyEndpoint(BaseAPIView): +class ProjectUserDisplayPropertyEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id): issue_property = ProjectUserProperty.objects.get(user=request.user, project_id=project_id) @@ -725,13 +725,13 @@ def patch(self, request, slug, project_id): issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters) issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties) issue_property.save() - serializer = IssueUserPropertySerializer(issue_property) + serializer = ProjectUserPropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_201_CREATED) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id): issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id) - serializer = IssueUserPropertySerializer(issue_property) + serializer = ProjectUserPropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_200_OK) From 280c3cec3d01483b4dc583d006b921723521334d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 1 Dec 2025 19:54:53 +0530 Subject: [PATCH 04/14] fix: enhance ProjectUserDisplayPropertyEndpoint to handle missing properties by creating new entries and improve response handling --- apps/api/plane/app/views/issue/base.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index b938dc7ea7a..79d34501b60 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -718,15 +718,25 @@ def destroy(self, request, slug, project_id, pk=None): class ProjectUserDisplayPropertyEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id): - issue_property = ProjectUserProperty.objects.get(user=request.user, project_id=project_id) + try: + issue_property = ProjectUserProperty.objects.get( + user=request.user, + project_id=project_id + ) + except ProjectUserProperty.DoesNotExist: + issue_property = ProjectUserProperty.objects.create( + user=request.user, + project_id=project_id + ) - issue_property.rich_filters = request.data.get("rich_filters", issue_property.rich_filters) - issue_property.filters = request.data.get("filters", issue_property.filters) - issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters) - issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties) - issue_property.save() - serializer = ProjectUserPropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_201_CREATED) + serializer = ProjectUserPropertySerializer( + issue_property, + data=request.data, + partial=True + ) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id): From e7cf8fa9254939c06a0eecb9421d073b4c528d3c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 3 Dec 2025 14:36:37 +0530 Subject: [PATCH 05/14] fix: correct formatting in migration for ProjectUserProperty model options --- .../plane/db/migrations/0113_alter_issueuserproperty_table.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py index 645633eb052..33365c3c255 100644 --- a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py +++ b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py @@ -34,7 +34,8 @@ class Migration(migrations.Migration): model_name='projectuserproperty', name='sort_order', field=models.FloatField(default=65535), - ), migrations.AlterModelOptions( + ), + migrations.AlterModelOptions( name='projectuserproperty', options={'ordering': ('-created_at',), 'verbose_name': 'Project User Property', 'verbose_name_plural': 'Project User Properties'}, ), From 2f0ea09f1537f098ff806daf882f5514d9ebb79e Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 Dec 2025 17:42:51 +0530 Subject: [PATCH 06/14] migrate: add migration to update existing non-service API tokens to remove workspace association --- ...114_alter_projectuserproperty_options_and_more.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py index 4fd35a93978..23520201181 100644 --- a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py +++ b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py @@ -28,6 +28,17 @@ def move_issue_user_properties_to_project_user_properties(apps, schema_editor): +def migrate_existing_api_tokens(apps, schema_editor): + APIToken = apps.get_model('db', 'APIToken') + + # Update all the existing non-service api tokens to not have a workspace + APIToken.objects.filter(is_service=False).update( + workspace_id=None, + user__is_bot=False + ) + return + + class Migration(migrations.Migration): dependencies = [ @@ -36,4 +47,5 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(move_issue_user_properties_to_project_user_properties, reverse_code=migrations.RunPython.noop), + migrations.RunPython(migrate_existing_api_tokens, reverse_code=migrations.RunPython.noop), ] From ab9a6b4466b1cd0384271257f310f74f5dc1c094 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 Dec 2025 17:50:59 +0530 Subject: [PATCH 07/14] migrate: refine migration to update existing non-service API tokens by excluding bot users from workspace removal --- .../0114_alter_projectuserproperty_options_and_more.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py index 23520201181..75714243fc7 100644 --- a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py +++ b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py @@ -32,9 +32,9 @@ def migrate_existing_api_tokens(apps, schema_editor): APIToken = apps.get_model('db', 'APIToken') # Update all the existing non-service api tokens to not have a workspace - APIToken.objects.filter(is_service=False).update( + APIToken.objects.filter(is_service=False, user__is_bot=False).update( workspace_id=None, - user__is_bot=False + ) return From d75efb3cfd1c896c1c81b113ff7c1da851c6d8b3 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 18 Dec 2025 18:11:53 +0530 Subject: [PATCH 08/14] chore: changed the project sort order in project user property --- apps/api/plane/api/views/project.py | 7 ----- apps/api/plane/app/views/project/base.py | 5 ---- apps/api/plane/app/views/project/invite.py | 2 +- apps/api/plane/app/views/project/member.py | 26 +++++++++---------- .../commands/create_project_member.py | 13 ++-------- apps/api/plane/db/models/project.py | 24 ++++++++++------- 6 files changed, 31 insertions(+), 46 deletions(-) diff --git a/apps/api/plane/api/views/project.py b/apps/api/plane/api/views/project.py index 698343ded16..5db5ae1350e 100644 --- a/apps/api/plane/api/views/project.py +++ b/apps/api/plane/api/views/project.py @@ -216,8 +216,6 @@ def post(self, request, slug): # Add the user as Administrator to the project _ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20) - # Also create the issue property for the user - _ = ProjectUserProperty.objects.create(project_id=serializer.instance.id, user=request.user) if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str( request.user.id @@ -227,11 +225,6 @@ def post(self, request, slug): member_id=serializer.instance.project_lead, role=20, ) - # Also create the issue property for the user - ProjectUserProperty.objects.create( - project_id=serializer.instance.id, - user_id=serializer.instance.project_lead, - ) State.objects.bulk_create( [ diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index 504211760db..d2ed934212f 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -266,11 +266,6 @@ def create(self, request, slug): member_id=serializer.data["project_lead"], role=ROLE.ADMIN.value, ) - # Also create the issue property for the user - ProjectUserProperty.objects.create( - project_id=serializer.data["id"], - user_id=serializer.data["project_lead"], - ) State.objects.bulk_create( [ diff --git a/apps/api/plane/app/views/project/invite.py b/apps/api/plane/app/views/project/invite.py index 6ae5a5e11a5..f4ea9ba41a1 100644 --- a/apps/api/plane/app/views/project/invite.py +++ b/apps/api/plane/app/views/project/invite.py @@ -220,7 +220,7 @@ def post(self, request, slug, project_id, pk): if project_member is None: # Create a Project Member _ = ProjectMember.objects.create( - workspace_id=project_invite.workspace_id, + project_id=project_id, member=user, role=project_invite.role, ) diff --git a/apps/api/plane/app/views/project/member.py b/apps/api/plane/app/views/project/member.py index 664355a17a6..ebb2a830f6d 100644 --- a/apps/api/plane/app/views/project/member.py +++ b/apps/api/plane/app/views/project/member.py @@ -1,6 +1,7 @@ # Third Party imports from rest_framework.response import Response from rest_framework import status +from django.db.models import Min # Module imports from .base import BaseViewSet, BaseAPIView @@ -89,24 +90,23 @@ def create(self, request, slug, project_id): # Update the roles of the existing members ProjectMember.objects.bulk_update(bulk_project_members, ["is_active", "role"], batch_size=100) - # Get the list of project members of the requested workspace with the given slug - project_members = ( - ProjectMember.objects.filter( + # Get the minimum sort_order for each member in the workspace + member_sort_orders = ( + ProjectUserProperty.objects.filter( workspace__slug=slug, - member_id__in=[member.get("member_id") for member in members], + user_id__in=[member.get("member_id") for member in members], ) - .values("member_id", "sort_order") - .order_by("sort_order") + .values("user_id") + .annotate(min_sort_order=Min("sort_order")) ) + # Convert to dictionary for easy lookup: {user_id: min_sort_order} + sort_order_map = {str(item["user_id"]): item["min_sort_order"] for item in member_sort_orders} # Loop through requested members for member in members: - # Get the sort orders of the member - sort_order = [ - project_member.get("sort_order") - for project_member in project_members - if str(project_member.get("member_id")) == str(member.get("member_id")) - ] + member_id = str(member.get("member_id")) + # Get the minimum sort_order for this member, or use default + min_sort_order = sort_order_map.get(member_id) # Create a new project member bulk_project_members.append( ProjectMember( @@ -114,7 +114,6 @@ def create(self, request, slug, project_id): role=member.get("role", 5), project_id=project_id, workspace_id=project.workspace_id, - sort_order=(sort_order[0] - 10000 if len(sort_order) else 65535), ) ) # Create a new issue property @@ -123,6 +122,7 @@ def create(self, request, slug, project_id): user_id=member.get("member_id"), project_id=project_id, workspace_id=project.workspace_id, + sort_order=(min_sort_order - 10000 if min_sort_order is not None else 65535), ) ) diff --git a/apps/api/plane/db/management/commands/create_project_member.py b/apps/api/plane/db/management/commands/create_project_member.py index a097056b99b..c04dbda7e7c 100644 --- a/apps/api/plane/db/management/commands/create_project_member.py +++ b/apps/api/plane/db/management/commands/create_project_member.py @@ -47,24 +47,15 @@ def handle(self, *args: Any, **options: Any): if not WorkspaceMember.objects.filter(workspace=project.workspace, member=user, is_active=True).exists(): raise CommandError("User not member in workspace") - # Get the smallest sort order - smallest_sort_order = ( - ProjectMember.objects.filter(workspace_id=project.workspace_id).order_by("sort_order").first() - ) - - if smallest_sort_order: - sort_order = smallest_sort_order.sort_order - 1000 - else: - sort_order = 65535 if ProjectMember.objects.filter(project=project, member=user).exists(): # Update the project member ProjectMember.objects.filter(project=project, member=user).update( - is_active=True, sort_order=sort_order, role=role + is_active=True, role=role ) else: # Create the project member - ProjectMember.objects.create(project=project, member=user, role=role, sort_order=sort_order) + ProjectMember.objects.create(project=project, member=user, role=role) # Issue Property ProjectUserProperty.objects.get_or_create(user=user, project=project) diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index 580c8db2118..6cd8050178b 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -205,14 +205,20 @@ class ProjectMember(ProjectBaseModel): is_active = models.BooleanField(default=True) def save(self, *args, **kwargs): - if self._state.adding: - smallest_sort_order = ProjectMember.objects.filter( - workspace_id=self.project.workspace_id, member=self.member - ).aggregate(smallest=models.Min("sort_order"))["smallest"] - - # Project ordering - if smallest_sort_order is not None: - self.sort_order = smallest_sort_order - 10000 + if self._state.adding and self.member: + # Get the minimum sort_order for this member in the workspace + min_sort_order_result = ProjectUserProperty.objects.filter( + workspace_id=self.project.workspace_id, user=self.member + ).aggregate(min_sort_order=models.Min("sort_order")) + min_sort_order = min_sort_order_result.get("min_sort_order") + + # create project user property with project sort order + ProjectUserProperty.objects.create( + workspace_id=self.project.workspace_id, + project=self.project, + user=self.member, + sort_order=(min_sort_order - 10000 if min_sort_order is not None else 65535), + ) super(ProjectMember, self).save(*args, **kwargs) @@ -315,8 +321,8 @@ class Meta: class ProjectUserProperty(ProjectBaseModel): - from .issue import get_default_filters, get_default_display_filters, get_default_display_properties + user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, From 3d7212bb68b4ade55cbe878019f021f0f80a0853 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Wed, 24 Dec 2025 16:03:51 +0530 Subject: [PATCH 09/14] chore: remove allowed_rate_limit from APIToken --- .../db/migrations/0113_alter_issueuserproperty_table.py | 5 ----- apps/api/plane/db/models/api.py | 1 - 2 files changed, 6 deletions(-) diff --git a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py index 33365c3c255..746c5f9a2c0 100644 --- a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py +++ b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py @@ -20,11 +20,6 @@ class Migration(migrations.Migration): old_name='IssueUserProperty', new_name='ProjectUserProperty', ), - migrations.AddField( - model_name='apitoken', - name='allowed_rate_limit', - field=models.CharField(default='60/min', max_length=255), - ), migrations.AddField( model_name='projectuserproperty', name='preferences', diff --git a/apps/api/plane/db/models/api.py b/apps/api/plane/db/models/api.py index 75449a74283..7d040ebc284 100644 --- a/apps/api/plane/db/models/api.py +++ b/apps/api/plane/db/models/api.py @@ -32,7 +32,6 @@ class APIToken(BaseModel): workspace = models.ForeignKey("db.Workspace", related_name="api_tokens", on_delete=models.CASCADE, null=True) expired_at = models.DateTimeField(blank=True, null=True) is_service = models.BooleanField(default=False) - allowed_rate_limit = models.CharField(max_length=255, default="60/min") class Meta: verbose_name = "API Token" From da31afa2e4c1386f24ae888c1f2c675571012ede Mon Sep 17 00:00:00 2001 From: vamsikrishnamathala Date: Fri, 2 Jan 2026 13:55:29 +0530 Subject: [PATCH 10/14] chore: updated user-properties endpoint for frontend --- .../navigation/use-tab-preferences.ts | 31 ++++--- .../layouts/auth-layout/project-wrapper.tsx | 4 +- .../web/core/services/issue_filter.service.ts | 20 ----- .../project/project-member.service.ts | 33 +------- .../core/services/project/project.service.ts | 20 +++-- .../core/store/issue/project/filter.store.ts | 14 ++-- .../project/base-project-member.store.ts | 84 ++++++++----------- apps/web/core/store/project/project.store.ts | 2 +- packages/types/src/view-props.ts | 8 ++ 9 files changed, 84 insertions(+), 132 deletions(-) diff --git a/apps/web/core/components/navigation/use-tab-preferences.ts b/apps/web/core/components/navigation/use-tab-preferences.ts index 57a61009436..c8942a3ebd5 100644 --- a/apps/web/core/components/navigation/use-tab-preferences.ts +++ b/apps/web/core/components/navigation/use-tab-preferences.ts @@ -23,7 +23,7 @@ export type TTabPreferencesHook = { */ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTabPreferencesHook => { const { - project: { getProjectMemberPreferences, updateProjectMemberPreferences }, + project: { getProjectUserProperties, updateProjectUserProperties }, } = useMember(); // const { projectUserInfo } = useUserPermissions(); const { data } = useUser(); @@ -33,21 +33,17 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa const memberId = data?.id || null; // Get preferences from store - const storePreferences = getProjectMemberPreferences(projectId); + const storePreferences = getProjectUserProperties(projectId); + const defaultTab = storePreferences?.preferences?.navigation?.default_tab || DEFAULT_TAB_KEY; + const hideInMoreMenu = storePreferences?.preferences?.navigation?.hide_in_more_menu || []; // Convert store preferences to component format const tabPreferences: TTabPreferences = useMemo(() => { - if (storePreferences) { - return { - defaultTab: storePreferences.default_tab || DEFAULT_TAB_KEY, - hiddenTabs: storePreferences.hide_in_more_menu || [], - }; - } return { - defaultTab: DEFAULT_TAB_KEY, - hiddenTabs: [], + defaultTab, + hiddenTabs: hideInMoreMenu, }; - }, [storePreferences]); + }, [defaultTab, hideInMoreMenu]); const isLoading = !storePreferences && memberId !== null; @@ -55,11 +51,13 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa * Update preferences via store */ const updatePreferences = async (newPreferences: TTabPreferences) => { - if (!memberId) return; - - await updateProjectMemberPreferences(workspaceSlug, projectId, memberId, { - default_tab: newPreferences.defaultTab, - hide_in_more_menu: newPreferences.hiddenTabs, + await updateProjectUserProperties(workspaceSlug, projectId, { + preferences: { + navigation: { + default_tab: newPreferences.defaultTab, + hide_in_more_menu: newPreferences.hiddenTabs, + }, + }, }); }; @@ -77,6 +75,7 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa title: "Success!", message: "Default tab updated successfully.", }); + return; }) .catch(() => { setToast({ diff --git a/apps/web/core/layouts/auth-layout/project-wrapper.tsx b/apps/web/core/layouts/auth-layout/project-wrapper.tsx index 6df49cbea59..fbbe1807ccd 100644 --- a/apps/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/apps/web/core/layouts/auth-layout/project-wrapper.tsx @@ -52,7 +52,7 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE); const { fetchViews } = useProjectView(); const { - project: { fetchProjectMembers, fetchProjectMemberPreferences }, + project: { fetchProjectMembers, fetchProjectUserProperties }, } = useMember(); const { fetchProjectStates, fetchProjectIntakeState } = useProjectState(); const { data: currentUserData } = useUser(); @@ -83,7 +83,7 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP // fetching project member preferences useSWR( currentUserData?.id ? PROJECT_MEMBER_PREFERENCES(projectId, currentProjectRole) : null, - currentUserData?.id ? () => fetchProjectMemberPreferences(workspaceSlug, projectId, currentUserData.id) : null, + currentUserData?.id ? () => fetchProjectUserProperties(workspaceSlug, projectId) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching project labels diff --git a/apps/web/core/services/issue_filter.service.ts b/apps/web/core/services/issue_filter.service.ts index d6f67b108ce..c81d7e43368 100644 --- a/apps/web/core/services/issue_filter.service.ts +++ b/apps/web/core/services/issue_filter.service.ts @@ -28,26 +28,6 @@ export class IssueFiltersService extends APIService { // }); // } - // project issue filters - async fetchProjectIssueFilters(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - async patchProjectIssueFilters( - workspaceSlug: string, - projectId: string, - data: Partial - ): Promise { - return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - // epic issue filters async fetchProjectEpicFilters(workspaceSlug: string, projectId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/epics-user-properties/`) diff --git a/apps/web/core/services/project/project-member.service.ts b/apps/web/core/services/project/project-member.service.ts index 04d62d1537c..8c378afa415 100644 --- a/apps/web/core/services/project/project-member.service.ts +++ b/apps/web/core/services/project/project-member.service.ts @@ -1,12 +1,6 @@ // types import { API_BASE_URL } from "@plane/constants"; -import type { - IProjectBulkAddFormData, - IProjectMemberPreferencesFullResponse, - IProjectMemberPreferencesResponse, - IProjectMemberPreferencesUpdate, - TProjectMembership, -} from "@plane/types"; +import type { IProjectBulkAddFormData, TProjectMembership } from "@plane/types"; // services import { APIService } from "@/services/api.service"; @@ -71,31 +65,6 @@ export class ProjectMemberService extends APIService { throw error?.response?.data; }); } - - async getProjectMemberPreferences( - workspaceSlug: string, - projectId: string, - memberId: string - ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/preferences/member/${memberId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async updateProjectMemberPreferences( - workspaceSlug: string, - projectId: string, - memberId: string, - data: IProjectMemberPreferencesUpdate - ): Promise { - return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/preferences/member/${memberId}/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } } const projectMemberService = new ProjectMemberService(); diff --git a/apps/web/core/services/project/project.service.ts b/apps/web/core/services/project/project.service.ts index 5915c7608d9..f4e29cecfc9 100644 --- a/apps/web/core/services/project/project.service.ts +++ b/apps/web/core/services/project/project.service.ts @@ -1,6 +1,7 @@ import { API_BASE_URL } from "@plane/constants"; import type { GithubRepositoriesResponse, + IProjectUserPropertiesResponse, ISearchIssueResponse, TProjectAnalyticsCount, TProjectAnalyticsCountParams, @@ -90,14 +91,21 @@ export class ProjectService extends APIService { }); } - async setProjectView( + // User Properties + async getProjectUserProperties(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateProjectUserProperties( workspaceSlug: string, projectId: string, - data: { - sort_order?: number; - } - ): Promise { - await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data) + data: Partial + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/apps/web/core/store/issue/project/filter.store.ts b/apps/web/core/store/issue/project/filter.store.ts index 1768606c5e7..dbfc6817d3f 100644 --- a/apps/web/core/store/issue/project/filter.store.ts +++ b/apps/web/core/store/issue/project/filter.store.ts @@ -16,12 +16,12 @@ import type { } from "@plane/types"; import { EIssuesStoreType } from "@plane/types"; import { handleIssueQueryParamsByLayout } from "@plane/utils"; -import { IssueFiltersService } from "@/services/issue_filter.service"; import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store"; import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; // helpers // types import type { IIssueRootStore } from "../root.store"; +import { ProjectService } from "@/services/project"; // constants // services @@ -56,7 +56,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj // root store rootIssueStore: IIssueRootStore; // services - issueFilterService; + projectService; constructor(_rootStore: IIssueRootStore) { super(); @@ -74,7 +74,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj // root store this.rootIssueStore = _rootStore; // services - this.issueFilterService = new IssueFiltersService(); + this.projectService = new ProjectService(); } get issueFilters() { @@ -129,7 +129,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj ); fetchFilters = async (workspaceSlug: string, projectId: string) => { - const _filters = await this.issueFilterService.fetchProjectIssueFilters(workspaceSlug, projectId); + const _filters = await this.projectService.getProjectUserProperties(workspaceSlug, projectId); const richFilters = _filters?.rich_filters; const displayFilters = this.computedDisplayFilters(_filters?.display_filters); @@ -176,7 +176,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj }); this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation"); - await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, { + await this.projectService.updateProjectUserProperties(workspaceSlug, projectId, { rich_filters: filters, }); } catch (error) { @@ -238,7 +238,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation"); } - await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, { + await this.projectService.updateProjectUserProperties(workspaceSlug, projectId, { display_filters: _filters.displayFilters, }); @@ -258,7 +258,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj }); }); - await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, { + await this.projectService.updateProjectUserProperties(workspaceSlug, projectId, { display_properties: _filters.displayProperties, }); break; diff --git a/apps/web/core/store/member/project/base-project-member.store.ts b/apps/web/core/store/member/project/base-project-member.store.ts index eb135868aae..0a2bbeb356c 100644 --- a/apps/web/core/store/member/project/base-project-member.store.ts +++ b/apps/web/core/store/member/project/base-project-member.store.ts @@ -6,14 +6,14 @@ import { EUserPermissions } from "@plane/constants"; import type { EUserProjectRoles, IProjectBulkAddFormData, - IProjectMemberNavigationPreferences, + IProjectUserPropertiesResponse, IUserLite, TProjectMembership, } from "@plane/types"; // plane web imports import type { RootStore } from "@/plane-web/store/root.store"; // services -import { ProjectMemberService } from "@/services/project"; +import { ProjectMemberService, ProjectService } from "@/services/project"; // store import type { IProjectStore } from "@/store/project/project.store"; import type { IRouterStore } from "@/store/router.store"; @@ -36,8 +36,8 @@ export interface IBaseProjectMemberStore { projectMemberMap: { [projectId: string]: Record; }; - projectMemberPreferencesMap: { - [projectId: string]: IProjectMemberNavigationPreferences; + projectUserPropertiesMap: { + [projectId: string]: IProjectUserPropertiesResponse; }; // filters store filters: IProjectMemberFiltersStore; @@ -48,25 +48,20 @@ export interface IBaseProjectMemberStore { getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null; getProjectMemberIds: (projectId: string, includeGuestUsers: boolean) => string[] | null; getFilteredProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null; - getProjectMemberPreferences: (projectId: string) => IProjectMemberNavigationPreferences | null; + getProjectUserProperties: (projectId: string) => IProjectUserPropertiesResponse | null; // fetch actions fetchProjectMembers: ( workspaceSlug: string, projectId: string, clearExistingMembers?: boolean ) => Promise; - fetchProjectMemberPreferences: ( - workspaceSlug: string, - projectId: string, - memberId: string - ) => Promise; + fetchProjectUserProperties: (workspaceSlug: string, projectId: string) => Promise; // update actions - updateProjectMemberPreferences: ( + updateProjectUserProperties: ( workspaceSlug: string, projectId: string, - memberId: string, - preferences: IProjectMemberNavigationPreferences - ) => Promise; + data: Partial + ) => Promise; // bulk operation actions bulkAddMembersToProject: ( workspaceSlug: string, @@ -91,8 +86,8 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore projectMemberMap: { [projectId: string]: Record; } = {}; - projectMemberPreferencesMap: { - [projectId: string]: IProjectMemberNavigationPreferences; + projectUserPropertiesMap: { + [projectId: string]: IProjectUserPropertiesResponse; } = {}; // filters store filters: IProjectMemberFiltersStore; @@ -104,18 +99,19 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore rootStore: RootStore; // services projectMemberService; + projectService; constructor(_memberRoot: IMemberRootStore, _rootStore: RootStore) { makeObservable(this, { // observables projectMemberMap: observable, - projectMemberPreferencesMap: observable, + projectUserPropertiesMap: observable, // computed projectMemberIds: computed, // actions fetchProjectMembers: action, - fetchProjectMemberPreferences: action, - updateProjectMemberPreferences: action, + fetchProjectUserProperties: action, + updateProjectUserProperties: action, bulkAddMembersToProject: action, updateMemberRole: action, removeMemberFromProject: action, @@ -129,6 +125,7 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore this.filters = new ProjectMemberFiltersStore(); // services this.projectMemberService = new ProjectMemberService(); + this.projectService = new ProjectService(); } /** @@ -440,62 +437,53 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore * @description get project member preferences * @param projectId */ - getProjectMemberPreferences = computedFn( - (projectId: string): IProjectMemberNavigationPreferences | null => - this.projectMemberPreferencesMap[projectId] || null + getProjectUserProperties = computedFn( + (projectId: string): IProjectUserPropertiesResponse | null => this.projectUserPropertiesMap[projectId] || null ); /** * @description fetch project member preferences * @param workspaceSlug * @param projectId - * @param memberId + * @param data */ - fetchProjectMemberPreferences = async ( + fetchProjectUserProperties = async ( workspaceSlug: string, - projectId: string, - memberId: string - ): Promise => { - const response = await this.projectMemberService.getProjectMemberPreferences(workspaceSlug, projectId, memberId); - const preferences: IProjectMemberNavigationPreferences = { - default_tab: response.preferences.navigation.default_tab, - hide_in_more_menu: response.preferences.navigation.hide_in_more_menu || [], - }; + projectId: string + ): Promise => { + const response = await this.projectService.getProjectUserProperties(workspaceSlug, projectId); runInAction(() => { - set(this.projectMemberPreferencesMap, [projectId], preferences); + set(this.projectUserPropertiesMap, [projectId], response); }); - return preferences; + return response; }; /** * @description update project member preferences * @param workspaceSlug * @param projectId - * @param memberId - * @param preferences + * @param data */ - updateProjectMemberPreferences = async ( + updateProjectUserProperties = async ( workspaceSlug: string, projectId: string, - memberId: string, - preferences: IProjectMemberNavigationPreferences - ): Promise => { - const previousPreferences = this.projectMemberPreferencesMap[projectId]; + data: Partial + ): Promise => { + const previousProperties = this.projectUserPropertiesMap[projectId]; try { // Optimistically update the store runInAction(() => { - set(this.projectMemberPreferencesMap, [projectId], preferences); - }); - await this.projectMemberService.updateProjectMemberPreferences(workspaceSlug, projectId, memberId, { - navigation: preferences, + set(this.projectUserPropertiesMap, [projectId], data); }); + const response = await this.projectService.updateProjectUserProperties(workspaceSlug, projectId, data); + return response; } catch (error) { // Revert on error runInAction(() => { - if (previousPreferences) { - set(this.projectMemberPreferencesMap, [projectId], previousPreferences); + if (previousProperties) { + set(this.projectUserPropertiesMap, [projectId], previousProperties); } else { - unset(this.projectMemberPreferencesMap, [projectId]); + unset(this.projectUserPropertiesMap, [projectId]); } }); throw error; diff --git a/apps/web/core/store/project/project.store.ts b/apps/web/core/store/project/project.store.ts index 422fac379fa..51f64c6a62a 100644 --- a/apps/web/core/store/project/project.store.ts +++ b/apps/web/core/store/project/project.store.ts @@ -509,7 +509,7 @@ export class ProjectStore implements IProjectStore { runInAction(() => { set(this.projectMap, [projectId, "sort_order"], viewProps?.sort_order); }); - const response = await this.projectService.setProjectView(workspaceSlug, projectId, viewProps); + const response = await this.projectService.updateProjectUserProperties(workspaceSlug, projectId, viewProps); return response; } catch (error) { runInAction(() => { diff --git a/packages/types/src/view-props.ts b/packages/types/src/view-props.ts index aa90541a957..b91a1adecb8 100644 --- a/packages/types/src/view-props.ts +++ b/packages/types/src/view-props.ts @@ -1,3 +1,4 @@ +import type { IProjectMemberNavigationPreferences } from "./project"; import type { TIssue } from "./issues/issue"; import type { LOGICAL_OPERATOR, TSupportedOperators } from "./rich-filters"; import type { CompleteOrEmpty } from "./utils"; @@ -194,6 +195,13 @@ export interface IIssueFiltersResponse { display_properties: IIssueDisplayProperties; } +export interface IProjectUserPropertiesResponse extends IIssueFiltersResponse { + sort_order?: number; + preferences?: { + navigation: IProjectMemberNavigationPreferences; + }; +} + export interface IWorkspaceUserPropertiesResponse extends IIssueFiltersResponse { navigation_project_limit?: number; navigation_control_preference?: "ACCORDION" | "TABBED"; From b39c769ca5cb72b21d199b1bd856b3848c384699 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 5 Jan 2026 14:02:31 +0530 Subject: [PATCH 11/14] chore: removed the extra projectuserproperty --- apps/api/plane/app/views/project/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index 69660b4c1d1..e1d0c0c2a6e 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -251,8 +251,6 @@ def create(self, request, slug): member=request.user, role=ROLE.ADMIN.value, ) - # Also create the issue property for the user - _ = ProjectUserProperty.objects.create(project_id=serializer.data["id"], user=request.user) if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str( request.user.id From d3a50b78f0671181d4b3c8ce95dd7f39462f7370 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 5 Jan 2026 14:08:35 +0530 Subject: [PATCH 12/14] chore: updated the migration file --- ...rojectuserproperty_delete_issueuserproperty_and_more.py} | 6 +++--- ...perty_options_and_more.py => 0115_auto_20260105_0836.py} | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename apps/api/plane/db/migrations/{0113_alter_issueuserproperty_table.py => 0114_projectuserproperty_delete_issueuserproperty_and_more.py} (94%) rename apps/api/plane/db/migrations/{0114_alter_projectuserproperty_options_and_more.py => 0115_auto_20260105_0836.py} (93%) diff --git a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py b/apps/api/plane/db/migrations/0114_projectuserproperty_delete_issueuserproperty_and_more.py similarity index 94% rename from apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py rename to apps/api/plane/db/migrations/0114_projectuserproperty_delete_issueuserproperty_and_more.py index 746c5f9a2c0..9a18fbafca5 100644 --- a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py +++ b/apps/api/plane/db/migrations/0114_projectuserproperty_delete_issueuserproperty_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.25 on 2025-11-28 14:36 +# Generated by Django 4.2.22 on 2026-01-05 08:35 from django.db import migrations, models import plane.db.models.project @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ('db', '0112_auto_20251124_0603'), + ('db', '0113_webhook_version'), ] operations = [ @@ -47,4 +47,4 @@ class Migration(migrations.Migration): model_name='projectuserproperty', constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('user', 'project'), name='project_user_property_unique_user_project_when_deleted_at_null'), ), - ] + ] \ No newline at end of file diff --git a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py b/apps/api/plane/db/migrations/0115_auto_20260105_0836.py similarity index 93% rename from apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py rename to apps/api/plane/db/migrations/0115_auto_20260105_0836.py index 75714243fc7..b9ac71d4709 100644 --- a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py +++ b/apps/api/plane/db/migrations/0115_auto_20260105_0836.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.25 on 2025-12-01 13:33 +# Generated by Django 4.2.22 on 2026-01-05 08:36 from django.db import migrations @@ -42,7 +42,7 @@ def migrate_existing_api_tokens(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('db', '0113_alter_issueuserproperty_table'), + ('db', '0114_projectuserproperty_delete_issueuserproperty_and_more'), ] operations = [ From 2ce67a114f110fdc50bd239dc2a1b96cf2246065 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 5 Jan 2026 21:15:41 +0530 Subject: [PATCH 13/14] chore: code refactor --- apps/web/core/components/navigation/use-tab-preferences.ts | 1 + packages/types/src/project/projects.ts | 6 ------ packages/types/src/view-props.ts | 7 +++++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/web/core/components/navigation/use-tab-preferences.ts b/apps/web/core/components/navigation/use-tab-preferences.ts index c8942a3ebd5..b99a192d54c 100644 --- a/apps/web/core/components/navigation/use-tab-preferences.ts +++ b/apps/web/core/components/navigation/use-tab-preferences.ts @@ -53,6 +53,7 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa const updatePreferences = async (newPreferences: TTabPreferences) => { await updateProjectUserProperties(workspaceSlug, projectId, { preferences: { + ...(storePreferences?.preferences || {}), navigation: { default_tab: newPreferences.defaultTab, hide_in_more_menu: newPreferences.hiddenTabs, diff --git a/packages/types/src/project/projects.ts b/packages/types/src/project/projects.ts index 9da12b57033..4258cf725ac 100644 --- a/packages/types/src/project/projects.ts +++ b/packages/types/src/project/projects.ts @@ -74,12 +74,6 @@ export interface IProjectLite { logo_props: TLogoProps; } -export type ProjectPreferences = { - pages: { - block_display: boolean; - }; -}; - export interface IProjectMap { [id: string]: IProject; } diff --git a/packages/types/src/view-props.ts b/packages/types/src/view-props.ts index b91a1adecb8..40f22e07aea 100644 --- a/packages/types/src/view-props.ts +++ b/packages/types/src/view-props.ts @@ -196,8 +196,11 @@ export interface IIssueFiltersResponse { } export interface IProjectUserPropertiesResponse extends IIssueFiltersResponse { - sort_order?: number; - preferences?: { + sort_order: number; + preferences: { + pages: { + block_display: boolean; + }; navigation: IProjectMemberNavigationPreferences; }; } From ffb017fe6e48f1d1a5487d08d3c63949c97106bf Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 6 Jan 2026 15:12:24 +0530 Subject: [PATCH 14/14] fix: type error --- apps/web/core/components/navigation/use-tab-preferences.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/core/components/navigation/use-tab-preferences.ts b/apps/web/core/components/navigation/use-tab-preferences.ts index b99a192d54c..5edc2396ed8 100644 --- a/apps/web/core/components/navigation/use-tab-preferences.ts +++ b/apps/web/core/components/navigation/use-tab-preferences.ts @@ -53,7 +53,7 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa const updatePreferences = async (newPreferences: TTabPreferences) => { await updateProjectUserProperties(workspaceSlug, projectId, { preferences: { - ...(storePreferences?.preferences || {}), + pages: storePreferences?.preferences?.pages || { block_display: false }, navigation: { default_tab: newPreferences.defaultTab, hide_in_more_menu: newPreferences.hiddenTabs,