[WEB-5537]refactor: rename IssueUserProperty to ProjectUserProperty and update related references (#8206)

* refactor: rename IssueUserProperty to ProjectUserProperty and update related references across the codebase

* migrate: move issue user properties to project user properties and update related fields and constraints

* refactor: rename IssueUserPropertySerializer and IssueUserDisplayPropertyEndpoint to ProjectUserPropertySerializer and ProjectUserDisplayPropertyEndpoint, updating all related references

* fix: enhance ProjectUserDisplayPropertyEndpoint to handle missing properties by creating new entries and improve response handling

* fix: correct formatting in migration for ProjectUserProperty model options

* migrate: add migration to update existing non-service API tokens to remove workspace association

* migrate: refine migration to update existing non-service API tokens by excluding bot users from workspace removal

* chore: changed the project sort order in project user property

* chore: remove allowed_rate_limit from APIToken

* chore: updated user-properties endpoint for frontend

* chore: removed the extra projectuserproperty

* chore: updated the migration file

* chore: code refactor

* fix: type error

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
Co-authored-by: vamsikrishnamathala <matalav55@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
Nikhil 2026-01-06 15:37:19 +05:30 committed by GitHub
parent 3d5e427894
commit ea1f92e0c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 304 additions and 256 deletions

View file

@ -18,7 +18,7 @@ from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
from plane.db.models import ( from plane.db.models import (
Cycle, Cycle,
Intake, Intake,
IssueUserProperty, ProjectUserProperty,
Module, Module,
Project, Project,
DeployBoard, DeployBoard,
@ -218,8 +218,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
# Add the user as Administrator to the project # Add the user as Administrator to the project
_ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20) _ = 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)
if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str( if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str(
request.user.id request.user.id
@ -229,11 +227,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
member_id=serializer.instance.project_lead, member_id=serializer.instance.project_lead,
role=20, role=20,
) )
# Also create the issue property for the user
IssueUserProperty.objects.create(
project_id=serializer.instance.id,
user_id=serializer.instance.project_lead,
)
State.objects.bulk_create( State.objects.bulk_create(
[ [

View file

@ -52,7 +52,7 @@ from .issue import (
IssueCreateSerializer, IssueCreateSerializer,
IssueActivitySerializer, IssueActivitySerializer,
IssueCommentSerializer, IssueCommentSerializer,
IssueUserPropertySerializer, ProjectUserPropertySerializer,
IssueAssigneeSerializer, IssueAssigneeSerializer,
LabelSerializer, LabelSerializer,
IssueSerializer, IssueSerializer,

View file

@ -18,7 +18,7 @@ from plane.db.models import (
Issue, Issue,
IssueActivity, IssueActivity,
IssueComment, IssueComment,
IssueUserProperty, ProjectUserProperty,
IssueAssignee, IssueAssignee,
IssueSubscriber, IssueSubscriber,
IssueLabel, IssueLabel,
@ -346,9 +346,9 @@ class IssueActivitySerializer(BaseSerializer):
fields = "__all__" fields = "__all__"
class IssueUserPropertySerializer(BaseSerializer): class ProjectUserPropertySerializer(BaseSerializer):
class Meta: class Meta:
model = IssueUserProperty model = ProjectUserProperty
fields = "__all__" fields = "__all__"
read_only_fields = ["user", "workspace", "project"] read_only_fields = ["user", "workspace", "project"]

View file

@ -14,7 +14,7 @@ from plane.app.views import (
IssueReactionViewSet, IssueReactionViewSet,
IssueRelationViewSet, IssueRelationViewSet,
IssueSubscriberViewSet, IssueSubscriberViewSet,
IssueUserDisplayPropertyEndpoint, ProjectUserDisplayPropertyEndpoint,
IssueViewSet, IssueViewSet,
LabelViewSet, LabelViewSet,
BulkArchiveIssuesEndpoint, BulkArchiveIssuesEndpoint,
@ -208,13 +208,13 @@ urlpatterns = [
name="project-issue-comment-reactions", name="project-issue-comment-reactions",
), ),
## End Comment Reactions ## End Comment Reactions
## IssueUserProperty ## ProjectUserProperty
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/", "workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
IssueUserDisplayPropertyEndpoint.as_view(), ProjectUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties", name="project-issue-display-properties",
), ),
## IssueUserProperty End ## ProjectUserProperty End
## Issue Archives ## Issue Archives
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/", "workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",

View file

@ -114,7 +114,7 @@ from .asset.v2 import (
from .issue.base import ( from .issue.base import (
IssueListEndpoint, IssueListEndpoint,
IssueViewSet, IssueViewSet,
IssueUserDisplayPropertyEndpoint, ProjectUserDisplayPropertyEndpoint,
BulkDeleteIssuesEndpoint, BulkDeleteIssuesEndpoint,
DeletedIssuesListViewSet, DeletedIssuesListViewSet,
IssuePaginatedViewSet, IssuePaginatedViewSet,

View file

@ -34,7 +34,7 @@ from plane.app.serializers import (
IssueDetailSerializer, IssueDetailSerializer,
IssueListDetailSerializer, IssueListDetailSerializer,
IssueSerializer, IssueSerializer,
IssueUserPropertySerializer, ProjectUserPropertySerializer,
) )
from plane.bgtasks.issue_activities_task import issue_activity from plane.bgtasks.issue_activities_task import issue_activity
from plane.bgtasks.issue_description_version_task import issue_description_version_task from plane.bgtasks.issue_description_version_task import issue_description_version_task
@ -51,7 +51,7 @@ from plane.db.models import (
IssueReaction, IssueReaction,
IssueRelation, IssueRelation,
IssueSubscriber, IssueSubscriber,
IssueUserProperty, ProjectUserProperty,
ModuleIssue, ModuleIssue,
Project, Project,
ProjectMember, ProjectMember,
@ -723,23 +723,33 @@ class IssueViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class IssueUserDisplayPropertyEndpoint(BaseAPIView): class ProjectUserDisplayPropertyEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def patch(self, request, slug, project_id): def patch(self, request, slug, project_id):
issue_property = IssueUserProperty.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) serializer = ProjectUserPropertySerializer(
issue_property.filters = request.data.get("filters", issue_property.filters) issue_property,
issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters) data=request.data,
issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties) partial=True
issue_property.save() )
serializer = IssueUserPropertySerializer(issue_property) serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_201_CREATED) serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id): 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) serializer = ProjectUserPropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View file

@ -24,14 +24,15 @@ from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.db.models import ( from plane.db.models import (
UserFavorite, UserFavorite,
DeployBoard, DeployBoard,
ProjectUserProperty,
Intake, Intake,
IssueUserProperty,
Project, Project,
ProjectIdentifier, ProjectIdentifier,
ProjectMember, ProjectMember,
ProjectNetwork, ProjectNetwork,
State, State,
DEFAULT_STATES, DEFAULT_STATES,
UserFavorite,
Workspace, Workspace,
WorkspaceMember, WorkspaceMember,
) )
@ -250,8 +251,6 @@ class ProjectViewSet(BaseViewSet):
member=request.user, member=request.user,
role=ROLE.ADMIN.value, role=ROLE.ADMIN.value,
) )
# Also create the issue property for the user
_ = IssueUserProperty.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( if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(
request.user.id request.user.id
@ -261,11 +260,6 @@ class ProjectViewSet(BaseViewSet):
member_id=serializer.data["project_lead"], member_id=serializer.data["project_lead"],
role=ROLE.ADMIN.value, role=ROLE.ADMIN.value,
) )
# Also create the issue property for the user
IssueUserProperty.objects.create(
project_id=serializer.data["id"],
user_id=serializer.data["project_lead"],
)
State.objects.bulk_create( State.objects.bulk_create(
[ [

View file

@ -24,7 +24,7 @@ from plane.db.models import (
User, User,
WorkspaceMember, WorkspaceMember,
Project, Project,
IssueUserProperty, ProjectUserProperty,
) )
from plane.db.models.project import ProjectNetwork from plane.db.models.project import ProjectNetwork
from plane.utils.host import base_host from plane.utils.host import base_host
@ -160,9 +160,9 @@ class UserProjectInvitationsViewset(BaseViewSet):
ignore_conflicts=True, ignore_conflicts=True,
) )
IssueUserProperty.objects.bulk_create( ProjectUserProperty.objects.bulk_create(
[ [
IssueUserProperty( ProjectUserProperty(
project_id=project_id, project_id=project_id,
user=request.user, user=request.user,
workspace=workspace, workspace=workspace,
@ -220,7 +220,7 @@ class ProjectJoinEndpoint(BaseAPIView):
if project_member is None: if project_member is None:
# Create a Project Member # Create a Project Member
_ = ProjectMember.objects.create( _ = ProjectMember.objects.create(
workspace_id=project_invite.workspace_id, project_id=project_id,
member=user, member=user,
role=project_invite.role, role=project_invite.role,
) )

View file

@ -1,6 +1,7 @@
# Third Party imports # Third Party imports
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from django.db.models import Min
# Module imports # Module imports
from .base import BaseViewSet, BaseAPIView from .base import BaseViewSet, BaseAPIView
@ -13,7 +14,7 @@ from plane.app.serializers import (
from plane.app.permissions import WorkspaceUserPermission 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.bgtasks.project_add_user_email_task import project_add_user_email
from plane.utils.host import base_host from plane.utils.host import base_host
from plane.app.permissions.base import allow_permission, ROLE from plane.app.permissions.base import allow_permission, ROLE
@ -89,24 +90,23 @@ class ProjectMemberViewSet(BaseViewSet):
# Update the roles of the existing members # Update the roles of the existing members
ProjectMember.objects.bulk_update(bulk_project_members, ["is_active", "role"], batch_size=100) 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 # Get the minimum sort_order for each member in the workspace
project_members = ( member_sort_orders = (
ProjectMember.objects.filter( ProjectUserProperty.objects.filter(
workspace__slug=slug, 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") .values("user_id")
.order_by("sort_order") .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 # Loop through requested members
for member in members: for member in members:
# Get the sort orders of the member member_id = str(member.get("member_id"))
sort_order = [ # Get the minimum sort_order for this member, or use default
project_member.get("sort_order") min_sort_order = sort_order_map.get(member_id)
for project_member in project_members
if str(project_member.get("member_id")) == str(member.get("member_id"))
]
# Create a new project member # Create a new project member
bulk_project_members.append( bulk_project_members.append(
ProjectMember( ProjectMember(
@ -114,22 +114,22 @@ class ProjectMemberViewSet(BaseViewSet):
role=member.get("role", 5), role=member.get("role", 5),
project_id=project_id, project_id=project_id,
workspace_id=project.workspace_id, workspace_id=project.workspace_id,
sort_order=(sort_order[0] - 10000 if len(sort_order) else 65535),
) )
) )
# Create a new issue property # Create a new issue property
bulk_issue_props.append( bulk_issue_props.append(
IssueUserProperty( ProjectUserProperty(
user_id=member.get("member_id"), user_id=member.get("member_id"),
project_id=project_id, project_id=project_id,
workspace_id=project.workspace_id, workspace_id=project.workspace_id,
sort_order=(min_sort_order - 10000 if min_sort_order is not None else 65535),
) )
) )
# Bulk create the project members and issue properties # Bulk create the project members and issue properties
project_members = ProjectMember.objects.bulk_create(bulk_project_members, batch_size=10, ignore_conflicts=True) 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_members = ProjectMember.objects.filter(
project_id=project_id, project_id=project_id,

View file

@ -21,7 +21,7 @@ from plane.db.models import (
WorkspaceMember, WorkspaceMember,
Project, Project,
ProjectMember, ProjectMember,
IssueUserProperty, ProjectUserProperty,
State, State,
Label, Label,
Issue, Issue,
@ -122,9 +122,9 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
) )
# Create issue user properties # Create issue user properties
IssueUserProperty.objects.bulk_create( ProjectUserProperty.objects.bulk_create(
[ [
IssueUserProperty( ProjectUserProperty(
project=project, project=project,
user_id=workspace_member["member_id"], user_id=workspace_member["member_id"],
workspace_id=workspace.id, workspace_id=workspace.id,

View file

@ -8,7 +8,7 @@ from plane.db.models import (
WorkspaceMember, WorkspaceMember,
ProjectMember, ProjectMember,
Project, Project,
IssueUserProperty, ProjectUserProperty,
) )
@ -47,27 +47,18 @@ class Command(BaseCommand):
if not WorkspaceMember.objects.filter(workspace=project.workspace, member=user, is_active=True).exists(): if not WorkspaceMember.objects.filter(workspace=project.workspace, member=user, is_active=True).exists():
raise CommandError("User not member in workspace") 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(): if ProjectMember.objects.filter(project=project, member=user).exists():
# Update the project member # Update the project member
ProjectMember.objects.filter(project=project, member=user).update( ProjectMember.objects.filter(project=project, member=user).update(
is_active=True, sort_order=sort_order, role=role is_active=True, role=role
) )
else: else:
# Create the project member # 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 # Issue Property
IssueUserProperty.objects.get_or_create(user=user, project=project) ProjectUserProperty.objects.get_or_create(user=user, project=project)
# Success message # Success message
self.stdout.write(self.style.SUCCESS(f"User {user_email} added to project {project_id}")) self.stdout.write(self.style.SUCCESS(f"User {user_email} added to project {project_id}"))

View file

@ -0,0 +1,50 @@
# Generated by Django 4.2.22 on 2026-01-05 08:35
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):
dependencies = [
('db', '0113_webhook_version'),
]
operations = [
migrations.AlterModelTable(
name='issueuserproperty',
table='project_user_properties',
),
migrations.RenameModel(
old_name='IssueUserProperty',
new_name='ProjectUserProperty',
),
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),
),
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'),
),
]

View file

@ -0,0 +1,51 @@
# Generated by Django 4.2.22 on 2026-01-05 08:36
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)
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, user__is_bot=False).update(
workspace_id=None,
)
return
class Migration(migrations.Migration):
dependencies = [
('db', '0114_projectuserproperty_delete_issueuserproperty_and_more'),
]
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),
]

View file

@ -34,7 +34,6 @@ from .issue import (
IssueLabel, IssueLabel,
IssueLink, IssueLink,
IssueMention, IssueMention,
IssueUserProperty,
IssueReaction, IssueReaction,
IssueRelation, IssueRelation,
IssueSequence, IssueSequence,
@ -54,6 +53,7 @@ from .project import (
ProjectMemberInvite, ProjectMemberInvite,
ProjectNetwork, ProjectNetwork,
ProjectPublicMember, ProjectPublicMember,
ProjectUserProperty,
) )
from .session import Session from .session import Session
from .social_connection import SocialLoginConnection from .social_connection import SocialLoginConnection

View file

@ -526,36 +526,6 @@ class IssueComment(ChangeTrackerMixin, ProjectBaseModel):
return str(self.issue) 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): class IssueLabel(ProjectBaseModel):
issue = models.ForeignKey("db.Issue", on_delete=models.CASCADE, related_name="label_issue") 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") label = models.ForeignKey("db.Label", on_delete=models.CASCADE, related_name="label_issue")

View file

@ -12,7 +12,6 @@ from django.db.models import Q
# Module imports # Module imports
from plane.db.mixins import AuditModel from plane.db.mixins import AuditModel
# Module imports
from .base import BaseModel from .base import BaseModel
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest")) ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
@ -219,14 +218,20 @@ class ProjectMember(ProjectBaseModel):
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self._state.adding: if self._state.adding and self.member:
smallest_sort_order = ProjectMember.objects.filter( # Get the minimum sort_order for this member in the workspace
workspace_id=self.project.workspace_id, member=self.member min_sort_order_result = ProjectUserProperty.objects.filter(
).aggregate(smallest=models.Min("sort_order"))["smallest"] 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")
# Project ordering # create project user property with project sort order
if smallest_sort_order is not None: ProjectUserProperty.objects.create(
self.sort_order = smallest_sort_order - 10000 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) super(ProjectMember, self).save(*args, **kwargs)
@ -326,3 +331,37 @@ class ProjectPublicMember(ProjectBaseModel):
verbose_name_plural = "Project Public Members" verbose_name_plural = "Project Public Members"
db_table = "project_public_members" db_table = "project_public_members"
ordering = ("-created_at",) 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="project_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 = "Project User Property"
verbose_name_plural = "Project 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="project_user_property_unique_user_project_when_deleted_at_null",
)
]
def __str__(self):
"""Return properties status of the project"""
return str(self.user)

View file

@ -6,7 +6,7 @@ from django.utils import timezone
from plane.db.models import ( from plane.db.models import (
Project, Project,
ProjectMember, ProjectMember,
IssueUserProperty, ProjectUserProperty,
State, State,
WorkspaceMember, WorkspaceMember,
User, User,
@ -82,8 +82,8 @@ class TestProjectAPIPost(TestProjectBase):
assert project_member.role == 20 # Administrator assert project_member.role == 20 # Administrator
assert project_member.is_active is True assert project_member.is_active is True
# Verify IssueUserProperty was created # Verify ProjectUserProperty was created
assert IssueUserProperty.objects.filter(project=project, user=user).exists() assert ProjectUserProperty.objects.filter(project=project, user=user).exists()
# Verify default states were created # Verify default states were created
states = State.objects.filter(project=project) states = State.objects.filter(project=project)
@ -116,8 +116,8 @@ class TestProjectAPIPost(TestProjectBase):
project = Project.objects.get(name=project_data["name"]) project = Project.objects.get(name=project_data["name"])
assert ProjectMember.objects.filter(project=project, role=20).count() == 2 assert ProjectMember.objects.filter(project=project, role=20).count() == 2
# Verify both have IssueUserProperty # Verify both have ProjectUserProperty
assert IssueUserProperty.objects.filter(project=project).count() == 2 assert ProjectUserProperty.objects.filter(project=project).count() == 2
@pytest.mark.django_db @pytest.mark.django_db
def test_create_project_guest_forbidden(self, session_client, workspace): def test_create_project_guest_forbidden(self, session_client, workspace):

View file

@ -23,7 +23,7 @@ export type TTabPreferencesHook = {
*/ */
export const useTabPreferences = (workspaceSlug: string, projectId: string): TTabPreferencesHook => { export const useTabPreferences = (workspaceSlug: string, projectId: string): TTabPreferencesHook => {
const { const {
project: { getProjectMemberPreferences, updateProjectMemberPreferences }, project: { getProjectUserProperties, updateProjectUserProperties },
} = useMember(); } = useMember();
// const { projectUserInfo } = useUserPermissions(); // const { projectUserInfo } = useUserPermissions();
const { data } = useUser(); const { data } = useUser();
@ -33,21 +33,17 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa
const memberId = data?.id || null; const memberId = data?.id || null;
// Get preferences from store // 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 // Convert store preferences to component format
const tabPreferences: TTabPreferences = useMemo(() => { const tabPreferences: TTabPreferences = useMemo(() => {
if (storePreferences) {
return {
defaultTab: storePreferences.default_tab || DEFAULT_TAB_KEY,
hiddenTabs: storePreferences.hide_in_more_menu || [],
};
}
return { return {
defaultTab: DEFAULT_TAB_KEY, defaultTab,
hiddenTabs: [], hiddenTabs: hideInMoreMenu,
}; };
}, [storePreferences]); }, [defaultTab, hideInMoreMenu]);
const isLoading = !storePreferences && memberId !== null; const isLoading = !storePreferences && memberId !== null;
@ -55,11 +51,14 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa
* Update preferences via store * Update preferences via store
*/ */
const updatePreferences = async (newPreferences: TTabPreferences) => { const updatePreferences = async (newPreferences: TTabPreferences) => {
if (!memberId) return; await updateProjectUserProperties(workspaceSlug, projectId, {
preferences: {
await updateProjectMemberPreferences(workspaceSlug, projectId, memberId, { pages: storePreferences?.preferences?.pages || { block_display: false },
default_tab: newPreferences.defaultTab, navigation: {
hide_in_more_menu: newPreferences.hiddenTabs, default_tab: newPreferences.defaultTab,
hide_in_more_menu: newPreferences.hiddenTabs,
},
},
}); });
}; };
@ -77,6 +76,7 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa
title: "Success!", title: "Success!",
message: "Default tab updated successfully.", message: "Default tab updated successfully.",
}); });
return;
}) })
.catch(() => { .catch(() => {
setToast({ setToast({

View file

@ -52,7 +52,7 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE); const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE);
const { fetchViews } = useProjectView(); const { fetchViews } = useProjectView();
const { const {
project: { fetchProjectMembers, fetchProjectMemberPreferences }, project: { fetchProjectMembers, fetchProjectUserProperties },
} = useMember(); } = useMember();
const { fetchProjectStates, fetchProjectIntakeState } = useProjectState(); const { fetchProjectStates, fetchProjectIntakeState } = useProjectState();
const { data: currentUserData } = useUser(); const { data: currentUserData } = useUser();
@ -83,7 +83,7 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
// fetching project member preferences // fetching project member preferences
useSWR( useSWR(
currentUserData?.id ? PROJECT_MEMBER_PREFERENCES(projectId, currentProjectRole) : null, 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 } { revalidateIfStale: false, revalidateOnFocus: false }
); );
// fetching project labels // fetching project labels

View file

@ -28,26 +28,6 @@ export class IssueFiltersService extends APIService {
// }); // });
// } // }
// project issue filters
async fetchProjectIssueFilters(workspaceSlug: string, projectId: string): Promise<IIssueFiltersResponse> {
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<IIssueFiltersResponse>
): Promise<any> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
// epic issue filters // epic issue filters
async fetchProjectEpicFilters(workspaceSlug: string, projectId: string): Promise<IIssueFiltersResponse> { async fetchProjectEpicFilters(workspaceSlug: string, projectId: string): Promise<IIssueFiltersResponse> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/epics-user-properties/`) return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/epics-user-properties/`)

View file

@ -1,12 +1,6 @@
// types // types
import { API_BASE_URL } from "@plane/constants"; import { API_BASE_URL } from "@plane/constants";
import type { import type { IProjectBulkAddFormData, TProjectMembership } from "@plane/types";
IProjectBulkAddFormData,
IProjectMemberPreferencesFullResponse,
IProjectMemberPreferencesResponse,
IProjectMemberPreferencesUpdate,
TProjectMembership,
} from "@plane/types";
// services // services
import { APIService } from "@/services/api.service"; import { APIService } from "@/services/api.service";
@ -71,31 +65,6 @@ export class ProjectMemberService extends APIService {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
async getProjectMemberPreferences(
workspaceSlug: string,
projectId: string,
memberId: string
): Promise<IProjectMemberPreferencesFullResponse> {
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<IProjectMemberPreferencesResponse> {
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(); const projectMemberService = new ProjectMemberService();

View file

@ -1,6 +1,7 @@
import { API_BASE_URL } from "@plane/constants"; import { API_BASE_URL } from "@plane/constants";
import type { import type {
GithubRepositoriesResponse, GithubRepositoriesResponse,
IProjectUserPropertiesResponse,
ISearchIssueResponse, ISearchIssueResponse,
TProjectAnalyticsCount, TProjectAnalyticsCount,
TProjectAnalyticsCountParams, TProjectAnalyticsCountParams,
@ -90,14 +91,21 @@ export class ProjectService extends APIService {
}); });
} }
async setProjectView( // User Properties
async getProjectUserProperties(workspaceSlug: string, projectId: string): Promise<IProjectUserPropertiesResponse> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async updateProjectUserProperties(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
data: { data: Partial<IProjectUserPropertiesResponse>
sort_order?: number; ): Promise<IProjectUserPropertiesResponse> {
} return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`, data)
): Promise<any> {
await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;

View file

@ -16,12 +16,12 @@ import type {
} from "@plane/types"; } from "@plane/types";
import { EIssuesStoreType } from "@plane/types"; import { EIssuesStoreType } from "@plane/types";
import { handleIssueQueryParamsByLayout } from "@plane/utils"; import { handleIssueQueryParamsByLayout } from "@plane/utils";
import { IssueFiltersService } from "@/services/issue_filter.service";
import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store"; import type { IBaseIssueFilterStore } from "../helpers/issue-filter-helper.store";
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
// helpers // helpers
// types // types
import type { IIssueRootStore } from "../root.store"; import type { IIssueRootStore } from "../root.store";
import { ProjectService } from "@/services/project";
// constants // constants
// services // services
@ -56,7 +56,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
// root store // root store
rootIssueStore: IIssueRootStore; rootIssueStore: IIssueRootStore;
// services // services
issueFilterService; projectService;
constructor(_rootStore: IIssueRootStore) { constructor(_rootStore: IIssueRootStore) {
super(); super();
@ -74,7 +74,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
// root store // root store
this.rootIssueStore = _rootStore; this.rootIssueStore = _rootStore;
// services // services
this.issueFilterService = new IssueFiltersService(); this.projectService = new ProjectService();
} }
get issueFilters() { get issueFilters() {
@ -129,7 +129,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
); );
fetchFilters = async (workspaceSlug: string, projectId: string) => { 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 richFilters = _filters?.rich_filters;
const displayFilters = this.computedDisplayFilters(_filters?.display_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"); this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, { await this.projectService.updateProjectUserProperties(workspaceSlug, projectId, {
rich_filters: filters, rich_filters: filters,
}); });
} catch (error) { } catch (error) {
@ -238,7 +238,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation"); this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
} }
await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, { await this.projectService.updateProjectUserProperties(workspaceSlug, projectId, {
display_filters: _filters.displayFilters, 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, display_properties: _filters.displayProperties,
}); });
break; break;

View file

@ -6,14 +6,14 @@ import { EUserPermissions } from "@plane/constants";
import type { import type {
EUserProjectRoles, EUserProjectRoles,
IProjectBulkAddFormData, IProjectBulkAddFormData,
IProjectMemberNavigationPreferences, IProjectUserPropertiesResponse,
IUserLite, IUserLite,
TProjectMembership, TProjectMembership,
} from "@plane/types"; } from "@plane/types";
// plane web imports // plane web imports
import type { RootStore } from "@/plane-web/store/root.store"; import type { RootStore } from "@/plane-web/store/root.store";
// services // services
import { ProjectMemberService } from "@/services/project"; import { ProjectMemberService, ProjectService } from "@/services/project";
// store // store
import type { IProjectStore } from "@/store/project/project.store"; import type { IProjectStore } from "@/store/project/project.store";
import type { IRouterStore } from "@/store/router.store"; import type { IRouterStore } from "@/store/router.store";
@ -36,8 +36,8 @@ export interface IBaseProjectMemberStore {
projectMemberMap: { projectMemberMap: {
[projectId: string]: Record<string, TProjectMembership>; [projectId: string]: Record<string, TProjectMembership>;
}; };
projectMemberPreferencesMap: { projectUserPropertiesMap: {
[projectId: string]: IProjectMemberNavigationPreferences; [projectId: string]: IProjectUserPropertiesResponse;
}; };
// filters store // filters store
filters: IProjectMemberFiltersStore; filters: IProjectMemberFiltersStore;
@ -48,25 +48,20 @@ export interface IBaseProjectMemberStore {
getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null; getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
getProjectMemberIds: (projectId: string, includeGuestUsers: boolean) => string[] | null; getProjectMemberIds: (projectId: string, includeGuestUsers: boolean) => string[] | null;
getFilteredProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null; getFilteredProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
getProjectMemberPreferences: (projectId: string) => IProjectMemberNavigationPreferences | null; getProjectUserProperties: (projectId: string) => IProjectUserPropertiesResponse | null;
// fetch actions // fetch actions
fetchProjectMembers: ( fetchProjectMembers: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
clearExistingMembers?: boolean clearExistingMembers?: boolean
) => Promise<TProjectMembership[]>; ) => Promise<TProjectMembership[]>;
fetchProjectMemberPreferences: ( fetchProjectUserProperties: (workspaceSlug: string, projectId: string) => Promise<IProjectUserPropertiesResponse>;
workspaceSlug: string,
projectId: string,
memberId: string
) => Promise<IProjectMemberNavigationPreferences>;
// update actions // update actions
updateProjectMemberPreferences: ( updateProjectUserProperties: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
memberId: string, data: Partial<IProjectUserPropertiesResponse>
preferences: IProjectMemberNavigationPreferences ) => Promise<IProjectUserPropertiesResponse>;
) => Promise<void>;
// bulk operation actions // bulk operation actions
bulkAddMembersToProject: ( bulkAddMembersToProject: (
workspaceSlug: string, workspaceSlug: string,
@ -91,8 +86,8 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
projectMemberMap: { projectMemberMap: {
[projectId: string]: Record<string, TProjectMembership>; [projectId: string]: Record<string, TProjectMembership>;
} = {}; } = {};
projectMemberPreferencesMap: { projectUserPropertiesMap: {
[projectId: string]: IProjectMemberNavigationPreferences; [projectId: string]: IProjectUserPropertiesResponse;
} = {}; } = {};
// filters store // filters store
filters: IProjectMemberFiltersStore; filters: IProjectMemberFiltersStore;
@ -104,18 +99,19 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
rootStore: RootStore; rootStore: RootStore;
// services // services
projectMemberService; projectMemberService;
projectService;
constructor(_memberRoot: IMemberRootStore, _rootStore: RootStore) { constructor(_memberRoot: IMemberRootStore, _rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
// observables // observables
projectMemberMap: observable, projectMemberMap: observable,
projectMemberPreferencesMap: observable, projectUserPropertiesMap: observable,
// computed // computed
projectMemberIds: computed, projectMemberIds: computed,
// actions // actions
fetchProjectMembers: action, fetchProjectMembers: action,
fetchProjectMemberPreferences: action, fetchProjectUserProperties: action,
updateProjectMemberPreferences: action, updateProjectUserProperties: action,
bulkAddMembersToProject: action, bulkAddMembersToProject: action,
updateMemberRole: action, updateMemberRole: action,
removeMemberFromProject: action, removeMemberFromProject: action,
@ -129,6 +125,7 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
this.filters = new ProjectMemberFiltersStore(); this.filters = new ProjectMemberFiltersStore();
// services // services
this.projectMemberService = new ProjectMemberService(); this.projectMemberService = new ProjectMemberService();
this.projectService = new ProjectService();
} }
/** /**
@ -440,62 +437,53 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
* @description get project member preferences * @description get project member preferences
* @param projectId * @param projectId
*/ */
getProjectMemberPreferences = computedFn( getProjectUserProperties = computedFn(
(projectId: string): IProjectMemberNavigationPreferences | null => (projectId: string): IProjectUserPropertiesResponse | null => this.projectUserPropertiesMap[projectId] || null
this.projectMemberPreferencesMap[projectId] || null
); );
/** /**
* @description fetch project member preferences * @description fetch project member preferences
* @param workspaceSlug * @param workspaceSlug
* @param projectId * @param projectId
* @param memberId * @param data
*/ */
fetchProjectMemberPreferences = async ( fetchProjectUserProperties = async (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string
memberId: string ): Promise<IProjectUserPropertiesResponse> => {
): Promise<IProjectMemberNavigationPreferences> => { const response = await this.projectService.getProjectUserProperties(workspaceSlug, projectId);
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 || [],
};
runInAction(() => { runInAction(() => {
set(this.projectMemberPreferencesMap, [projectId], preferences); set(this.projectUserPropertiesMap, [projectId], response);
}); });
return preferences; return response;
}; };
/** /**
* @description update project member preferences * @description update project member preferences
* @param workspaceSlug * @param workspaceSlug
* @param projectId * @param projectId
* @param memberId * @param data
* @param preferences
*/ */
updateProjectMemberPreferences = async ( updateProjectUserProperties = async (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
memberId: string, data: Partial<IProjectUserPropertiesResponse>
preferences: IProjectMemberNavigationPreferences ): Promise<IProjectUserPropertiesResponse> => {
): Promise<void> => { const previousProperties = this.projectUserPropertiesMap[projectId];
const previousPreferences = this.projectMemberPreferencesMap[projectId];
try { try {
// Optimistically update the store // Optimistically update the store
runInAction(() => { runInAction(() => {
set(this.projectMemberPreferencesMap, [projectId], preferences); set(this.projectUserPropertiesMap, [projectId], data);
});
await this.projectMemberService.updateProjectMemberPreferences(workspaceSlug, projectId, memberId, {
navigation: preferences,
}); });
const response = await this.projectService.updateProjectUserProperties(workspaceSlug, projectId, data);
return response;
} catch (error) { } catch (error) {
// Revert on error // Revert on error
runInAction(() => { runInAction(() => {
if (previousPreferences) { if (previousProperties) {
set(this.projectMemberPreferencesMap, [projectId], previousPreferences); set(this.projectUserPropertiesMap, [projectId], previousProperties);
} else { } else {
unset(this.projectMemberPreferencesMap, [projectId]); unset(this.projectUserPropertiesMap, [projectId]);
} }
}); });
throw error; throw error;

View file

@ -509,7 +509,7 @@ export class ProjectStore implements IProjectStore {
runInAction(() => { runInAction(() => {
set(this.projectMap, [projectId, "sort_order"], viewProps?.sort_order); 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; return response;
} catch (error) { } catch (error) {
runInAction(() => { runInAction(() => {

View file

@ -74,12 +74,6 @@ export interface IProjectLite {
logo_props: TLogoProps; logo_props: TLogoProps;
} }
export type ProjectPreferences = {
pages: {
block_display: boolean;
};
};
export interface IProjectMap { export interface IProjectMap {
[id: string]: IProject; [id: string]: IProject;
} }

View file

@ -1,3 +1,4 @@
import type { IProjectMemberNavigationPreferences } from "./project";
import type { TIssue } from "./issues/issue"; import type { TIssue } from "./issues/issue";
import type { LOGICAL_OPERATOR, TSupportedOperators } from "./rich-filters"; import type { LOGICAL_OPERATOR, TSupportedOperators } from "./rich-filters";
import type { CompleteOrEmpty } from "./utils"; import type { CompleteOrEmpty } from "./utils";
@ -194,6 +195,16 @@ export interface IIssueFiltersResponse {
display_properties: IIssueDisplayProperties; display_properties: IIssueDisplayProperties;
} }
export interface IProjectUserPropertiesResponse extends IIssueFiltersResponse {
sort_order: number;
preferences: {
pages: {
block_display: boolean;
};
navigation: IProjectMemberNavigationPreferences;
};
}
export interface IWorkspaceUserPropertiesResponse extends IIssueFiltersResponse { export interface IWorkspaceUserPropertiesResponse extends IIssueFiltersResponse {
navigation_project_limit?: number; navigation_project_limit?: number;
navigation_control_preference?: "ACCORDION" | "TABBED"; navigation_control_preference?: "ACCORDION" | "TABBED";