[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:
parent
3d5e427894
commit
ea1f92e0c6
27 changed files with 304 additions and 256 deletions
|
|
@ -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(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ from .issue import (
|
||||||
IssueCreateSerializer,
|
IssueCreateSerializer,
|
||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
IssueUserPropertySerializer,
|
ProjectUserPropertySerializer,
|
||||||
IssueAssigneeSerializer,
|
IssueAssigneeSerializer,
|
||||||
LabelSerializer,
|
LabelSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}"))
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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/`)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue