mirror of
https://github.com/makeplane/plane.git
synced 2026-02-04 13:09:15 -06:00
[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:
@@ -18,7 +18,7 @@ from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Intake,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
Module,
|
||||
Project,
|
||||
DeployBoard,
|
||||
@@ -218,8 +218,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# 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)
|
||||
|
||||
if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str(
|
||||
request.user.id
|
||||
@@ -229,11 +227,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
member_id=serializer.instance.project_lead,
|
||||
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(
|
||||
[
|
||||
|
||||
@@ -52,7 +52,7 @@ from .issue import (
|
||||
IssueCreateSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueUserPropertySerializer,
|
||||
ProjectUserPropertySerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.db.models import (
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
IssueLabel,
|
||||
@@ -346,9 +346,9 @@ class IssueActivitySerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IssueUserPropertySerializer(BaseSerializer):
|
||||
class ProjectUserPropertySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueUserProperty
|
||||
model = ProjectUserProperty
|
||||
fields = "__all__"
|
||||
read_only_fields = ["user", "workspace", "project"]
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from plane.app.views import (
|
||||
IssueReactionViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueSubscriberViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
ProjectUserDisplayPropertyEndpoint,
|
||||
IssueViewSet,
|
||||
LabelViewSet,
|
||||
BulkArchiveIssuesEndpoint,
|
||||
@@ -208,13 +208,13 @@ urlpatterns = [
|
||||
name="project-issue-comment-reactions",
|
||||
),
|
||||
## End Comment Reactions
|
||||
## IssueUserProperty
|
||||
## ProjectUserProperty
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
|
||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||
ProjectUserDisplayPropertyEndpoint.as_view(),
|
||||
name="project-issue-display-properties",
|
||||
),
|
||||
## IssueUserProperty End
|
||||
## ProjectUserProperty End
|
||||
## Issue Archives
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
|
||||
|
||||
@@ -114,7 +114,7 @@ from .asset.v2 import (
|
||||
from .issue.base import (
|
||||
IssueListEndpoint,
|
||||
IssueViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
ProjectUserDisplayPropertyEndpoint,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
DeletedIssuesListViewSet,
|
||||
IssuePaginatedViewSet,
|
||||
|
||||
@@ -34,7 +34,7 @@ from plane.app.serializers import (
|
||||
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
|
||||
@@ -51,7 +51,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
IssueRelation,
|
||||
IssueSubscriber,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
ModuleIssue,
|
||||
Project,
|
||||
ProjectMember,
|
||||
@@ -723,23 +723,33 @@ class IssueViewSet(BaseViewSet):
|
||||
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 = 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)
|
||||
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 = IssueUserPropertySerializer(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):
|
||||
issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
|
||||
serializer = ProjectUserPropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -24,14 +24,15 @@ from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from plane.db.models import (
|
||||
UserFavorite,
|
||||
DeployBoard,
|
||||
ProjectUserProperty,
|
||||
Intake,
|
||||
IssueUserProperty,
|
||||
Project,
|
||||
ProjectIdentifier,
|
||||
ProjectMember,
|
||||
ProjectNetwork,
|
||||
State,
|
||||
DEFAULT_STATES,
|
||||
UserFavorite,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
)
|
||||
@@ -250,8 +251,6 @@ class ProjectViewSet(BaseViewSet):
|
||||
member=request.user,
|
||||
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(
|
||||
request.user.id
|
||||
@@ -261,11 +260,6 @@ class ProjectViewSet(BaseViewSet):
|
||||
member_id=serializer.data["project_lead"],
|
||||
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(
|
||||
[
|
||||
|
||||
@@ -24,7 +24,7 @@ from plane.db.models import (
|
||||
User,
|
||||
WorkspaceMember,
|
||||
Project,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
)
|
||||
from plane.db.models.project import ProjectNetwork
|
||||
from plane.utils.host import base_host
|
||||
@@ -160,9 +160,9 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
IssueUserProperty.objects.bulk_create(
|
||||
ProjectUserProperty.objects.bulk_create(
|
||||
[
|
||||
IssueUserProperty(
|
||||
ProjectUserProperty(
|
||||
project_id=project_id,
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
@@ -220,7 +220,7 @@ class ProjectJoinEndpoint(BaseAPIView):
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -13,7 +14,7 @@ from plane.app.serializers import (
|
||||
|
||||
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
|
||||
@@ -89,24 +90,23 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
# 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,22 +114,22 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
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
|
||||
bulk_issue_props.append(
|
||||
IssueUserProperty(
|
||||
ProjectUserProperty(
|
||||
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),
|
||||
)
|
||||
)
|
||||
|
||||
# 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,
|
||||
|
||||
@@ -21,7 +21,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
Project,
|
||||
ProjectMember,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
State,
|
||||
Label,
|
||||
Issue,
|
||||
@@ -122,9 +122,9 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
|
||||
)
|
||||
|
||||
# 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,
|
||||
|
||||
@@ -8,7 +8,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
Project,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,27 +47,18 @@ class Command(BaseCommand):
|
||||
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
|
||||
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}"))
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
51
apps/api/plane/db/migrations/0115_auto_20260105_0836.py
Normal file
51
apps/api/plane/db/migrations/0115_auto_20260105_0836.py
Normal 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),
|
||||
]
|
||||
@@ -34,7 +34,6 @@ from .issue import (
|
||||
IssueLabel,
|
||||
IssueLink,
|
||||
IssueMention,
|
||||
IssueUserProperty,
|
||||
IssueReaction,
|
||||
IssueRelation,
|
||||
IssueSequence,
|
||||
@@ -54,6 +53,7 @@ from .project import (
|
||||
ProjectMemberInvite,
|
||||
ProjectNetwork,
|
||||
ProjectPublicMember,
|
||||
ProjectUserProperty,
|
||||
)
|
||||
from .session import Session
|
||||
from .social_connection import SocialLoginConnection
|
||||
|
||||
@@ -526,36 +526,6 @@ class IssueComment(ChangeTrackerMixin, ProjectBaseModel):
|
||||
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")
|
||||
|
||||
@@ -12,7 +12,6 @@ from django.db.models import Q
|
||||
# Module imports
|
||||
from plane.db.mixins import AuditModel
|
||||
|
||||
# Module imports
|
||||
from .base import BaseModel
|
||||
|
||||
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
|
||||
@@ -219,14 +218,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"]
|
||||
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")
|
||||
|
||||
# Project ordering
|
||||
if smallest_sort_order is not None:
|
||||
self.sort_order = smallest_sort_order - 10000
|
||||
# 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)
|
||||
|
||||
@@ -326,3 +331,37 @@ class ProjectPublicMember(ProjectBaseModel):
|
||||
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="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)
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.utils import timezone
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
State,
|
||||
WorkspaceMember,
|
||||
User,
|
||||
@@ -82,8 +82,8 @@ class TestProjectAPIPost(TestProjectBase):
|
||||
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 @@ class TestProjectAPIPost(TestProjectBase):
|
||||
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):
|
||||
|
||||
@@ -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,14 @@ 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: {
|
||||
pages: storePreferences?.preferences?.pages || { block_display: false },
|
||||
navigation: {
|
||||
default_tab: newPreferences.defaultTab,
|
||||
hide_in_more_menu: newPreferences.hiddenTabs,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -77,6 +76,7 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa
|
||||
title: "Success!",
|
||||
message: "Default tab updated successfully.",
|
||||
});
|
||||
return;
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
async fetchProjectEpicFilters(workspaceSlug: string, projectId: string): Promise<IIssueFiltersResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/epics-user-properties/`)
|
||||
|
||||
@@ -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<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();
|
||||
|
||||
@@ -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<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,
|
||||
projectId: string,
|
||||
data: {
|
||||
sort_order?: number;
|
||||
}
|
||||
): Promise<any> {
|
||||
await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data)
|
||||
data: Partial<IProjectUserPropertiesResponse>
|
||||
): Promise<IProjectUserPropertiesResponse> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, TProjectMembership>;
|
||||
};
|
||||
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<TProjectMembership[]>;
|
||||
fetchProjectMemberPreferences: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
memberId: string
|
||||
) => Promise<IProjectMemberNavigationPreferences>;
|
||||
fetchProjectUserProperties: (workspaceSlug: string, projectId: string) => Promise<IProjectUserPropertiesResponse>;
|
||||
// update actions
|
||||
updateProjectMemberPreferences: (
|
||||
updateProjectUserProperties: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
memberId: string,
|
||||
preferences: IProjectMemberNavigationPreferences
|
||||
) => Promise<void>;
|
||||
data: Partial<IProjectUserPropertiesResponse>
|
||||
) => Promise<IProjectUserPropertiesResponse>;
|
||||
// bulk operation actions
|
||||
bulkAddMembersToProject: (
|
||||
workspaceSlug: string,
|
||||
@@ -91,8 +86,8 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
|
||||
projectMemberMap: {
|
||||
[projectId: string]: Record<string, TProjectMembership>;
|
||||
} = {};
|
||||
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<IProjectMemberNavigationPreferences> => {
|
||||
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<IProjectUserPropertiesResponse> => {
|
||||
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<void> => {
|
||||
const previousPreferences = this.projectMemberPreferencesMap[projectId];
|
||||
data: Partial<IProjectUserPropertiesResponse>
|
||||
): Promise<IProjectUserPropertiesResponse> => {
|
||||
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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -74,12 +74,6 @@ export interface IProjectLite {
|
||||
logo_props: TLogoProps;
|
||||
}
|
||||
|
||||
export type ProjectPreferences = {
|
||||
pages: {
|
||||
block_display: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export interface IProjectMap {
|
||||
[id: string]: IProject;
|
||||
}
|
||||
|
||||
@@ -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,16 @@ export interface IIssueFiltersResponse {
|
||||
display_properties: IIssueDisplayProperties;
|
||||
}
|
||||
|
||||
export interface IProjectUserPropertiesResponse extends IIssueFiltersResponse {
|
||||
sort_order: number;
|
||||
preferences: {
|
||||
pages: {
|
||||
block_display: boolean;
|
||||
};
|
||||
navigation: IProjectMemberNavigationPreferences;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IWorkspaceUserPropertiesResponse extends IIssueFiltersResponse {
|
||||
navigation_project_limit?: number;
|
||||
navigation_control_preference?: "ACCORDION" | "TABBED";
|
||||
|
||||
Reference in New Issue
Block a user