Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0876ea6b55 | |||
| 7915121dd9 | |||
| 1e7f451d0b |
@@ -243,29 +243,6 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
):
|
||||
serializer = CycleSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
and request.data.get("external_source")
|
||||
and Cycle.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source"),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
cycle = Cycle.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
external_source=request.data.get("external_source"),
|
||||
external_id=request.data.get("external_id"),
|
||||
).first()
|
||||
return Response(
|
||||
{
|
||||
"error": "Cycle with the same external id and external source already exists",
|
||||
"cycle": str(cycle.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
serializer.save(
|
||||
project_id=project_id,
|
||||
owned_by=request.user,
|
||||
@@ -312,23 +289,6 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
serializer = CycleSerializer(cycle, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
and (cycle.external_id != request.data.get("external_id"))
|
||||
and Cycle.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source", cycle.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "Cycle with the same external id and external source already exists",
|
||||
"cycle_id": str(cycle.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -220,30 +220,6 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
and request.data.get("external_source")
|
||||
and Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source"),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
external_id=request.data.get("external_id"),
|
||||
external_source=request.data.get("external_source"),
|
||||
).first()
|
||||
return Response(
|
||||
{
|
||||
"error": "Issue with the same external id and external source already exists",
|
||||
"issue_id": str(issue.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
serializer.save()
|
||||
|
||||
# Track the issue
|
||||
@@ -280,24 +256,6 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
partial=True,
|
||||
)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
str(request.data.get("external_id"))
|
||||
and (issue.external_id != str(request.data.get("external_id")))
|
||||
and Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source", issue.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "Issue with the same external id and external source already exists",
|
||||
"issue_id": str(issue.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
@@ -305,8 +263,6 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
external_id__isnull=False,
|
||||
external_source__isnull=False,
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
|
||||
@@ -132,29 +132,6 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
},
|
||||
)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
and request.data.get("external_source")
|
||||
and Module.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source"),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
module = Module.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source"),
|
||||
external_id=request.data.get("external_id"),
|
||||
).first()
|
||||
return Response(
|
||||
{
|
||||
"error": "Module with the same external id and external source already exists",
|
||||
"module_id": str(module.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
serializer.save()
|
||||
module = Module.objects.get(pk=serializer.data["id"])
|
||||
serializer = ModuleSerializer(module)
|
||||
@@ -172,25 +149,8 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
partial=True,
|
||||
)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
and (module.external_id != request.data.get("external_id"))
|
||||
and Module.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source", module.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "Module with the same external id and external source already exists",
|
||||
"module_id": str(module.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
|
||||
@@ -38,30 +38,6 @@ class StateAPIEndpoint(BaseAPIView):
|
||||
data=request.data, context={"project_id": project_id}
|
||||
)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
and request.data.get("external_source")
|
||||
and State.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source"),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
state = State.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
external_id=request.data.get("external_id"),
|
||||
external_source=request.data.get("external_source"),
|
||||
).first()
|
||||
return Response(
|
||||
{
|
||||
"error": "State with the same external id and external source already exists",
|
||||
"state_id": str(state.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
serializer.save(project_id=project_id)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -115,23 +91,6 @@ class StateAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
str(request.data.get("external_id"))
|
||||
and (state.external_id != str(request.data.get("external_id")))
|
||||
and State.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source", state.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "State with the same external id and external source already exists",
|
||||
"state_id": str(state.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -7,6 +7,8 @@ from .user import (
|
||||
UserAdminLiteSerializer,
|
||||
UserMeSerializer,
|
||||
UserMeSettingsSerializer,
|
||||
ProfileSerializer,
|
||||
AccountSerializer,
|
||||
)
|
||||
from .workspace import (
|
||||
WorkSpaceSerializer,
|
||||
@@ -116,7 +118,10 @@ from .inbox import (
|
||||
|
||||
from .analytic import AnalyticViewSerializer
|
||||
|
||||
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
|
||||
from .notification import (
|
||||
NotificationSerializer,
|
||||
UserNotificationPreferenceSerializer,
|
||||
)
|
||||
|
||||
from .exporter import ExporterHistorySerializer
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class CycleWriteSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class CycleSerializer(BaseSerializer):
|
||||
owned_by = UserLiteSerializer(read_only=True)
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
cancelled_issues = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -3,7 +3,7 @@ from rest_framework import serializers
|
||||
|
||||
# Module import
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import User, Workspace, WorkspaceMemberInvite
|
||||
from plane.db.models import User, Workspace, WorkspaceMemberInvite, Profile, Account
|
||||
from plane.license.models import InstanceAdmin, Instance
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ class UserSerializer(BaseSerializer):
|
||||
"last_logout_ip",
|
||||
"last_login_uagent",
|
||||
"token_updated_at",
|
||||
"is_onboarded",
|
||||
"is_bot",
|
||||
"is_password_autoset",
|
||||
"is_email_verified",
|
||||
@@ -52,16 +51,9 @@ class UserMeSerializer(BaseSerializer):
|
||||
"is_bot",
|
||||
"is_email_verified",
|
||||
"is_managed",
|
||||
"is_onboarded",
|
||||
"is_tour_completed",
|
||||
"mobile_number",
|
||||
"role",
|
||||
"onboarding_step",
|
||||
"user_timezone",
|
||||
"username",
|
||||
"theme",
|
||||
"last_workspace_id",
|
||||
"use_case",
|
||||
"is_password_autoset",
|
||||
"is_email_verified",
|
||||
]
|
||||
@@ -84,25 +76,28 @@ class UserMeSettingsSerializer(BaseSerializer):
|
||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||
email=obj.email
|
||||
).count()
|
||||
|
||||
# profile
|
||||
profile = Profile.objects.get(user=obj)
|
||||
if (
|
||||
obj.last_workspace_id is not None
|
||||
profile.last_workspace_id is not None
|
||||
and Workspace.objects.filter(
|
||||
pk=obj.last_workspace_id,
|
||||
pk=profile.last_workspace_id,
|
||||
workspace_member__member=obj.id,
|
||||
workspace_member__is_active=True,
|
||||
).exists()
|
||||
):
|
||||
workspace = Workspace.objects.filter(
|
||||
pk=obj.last_workspace_id,
|
||||
pk=profile.last_workspace_id,
|
||||
workspace_member__member=obj.id,
|
||||
workspace_member__is_active=True,
|
||||
).first()
|
||||
return {
|
||||
"last_workspace_id": obj.last_workspace_id,
|
||||
"last_workspace_id": profile.last_workspace_id,
|
||||
"last_workspace_slug": workspace.slug
|
||||
if workspace is not None
|
||||
else "",
|
||||
"fallback_workspace_id": obj.last_workspace_id,
|
||||
"fallback_workspace_id": profile.last_workspace_id,
|
||||
"fallback_workspace_slug": workspace.slug
|
||||
if workspace is not None
|
||||
else "",
|
||||
@@ -197,3 +192,16 @@ class ResetPasswordSerializer(serializers.Serializer):
|
||||
"""
|
||||
|
||||
new_password = serializers.CharField(required=True, min_length=8)
|
||||
|
||||
class ProfileSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AccountSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = "__all__"
|
||||
@@ -3,6 +3,8 @@ from django.urls import path
|
||||
from plane.app.views import (
|
||||
## User
|
||||
UserEndpoint,
|
||||
ProfileEndpoint,
|
||||
AccountEndpoint,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
@@ -39,6 +41,16 @@ urlpatterns = [
|
||||
),
|
||||
name="users",
|
||||
),
|
||||
path(
|
||||
"users/me/profile/",
|
||||
ProfileEndpoint.as_view(),
|
||||
name="accounts",
|
||||
),
|
||||
path(
|
||||
"users/me/accounts/",
|
||||
AccountEndpoint.as_view(),
|
||||
name="accounts",
|
||||
),
|
||||
path(
|
||||
"users/me/instance-admin/",
|
||||
UserEndpoint.as_view(
|
||||
@@ -96,4 +108,4 @@ urlpatterns = [
|
||||
name="set-password",
|
||||
),
|
||||
## End User Graph
|
||||
]
|
||||
]
|
||||
@@ -18,6 +18,8 @@ from .user import (
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
ProfileEndpoint,
|
||||
AccountEndpoint,
|
||||
)
|
||||
|
||||
from .oauth import OauthEndpoint
|
||||
@@ -180,7 +182,4 @@ from .webhook import (
|
||||
WebhookSecretRegenerateEndpoint,
|
||||
)
|
||||
|
||||
from .dashboard import (
|
||||
DashboardEndpoint,
|
||||
WidgetsEndpoint
|
||||
)
|
||||
from .dashboard import DashboardEndpoint, WidgetsEndpoint
|
||||
|
||||
@@ -9,10 +9,19 @@ from plane.app.serializers import (
|
||||
IssueActivitySerializer,
|
||||
UserMeSerializer,
|
||||
UserMeSettingsSerializer,
|
||||
ProfileSerializer,
|
||||
AccountSerializer,
|
||||
)
|
||||
|
||||
from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember
|
||||
from plane.db.models import (
|
||||
Account,
|
||||
User,
|
||||
IssueActivity,
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
Profile,
|
||||
)
|
||||
from plane.license.models import Instance, InstanceAdmin
|
||||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
@@ -131,24 +140,29 @@ class UserEndpoint(BaseViewSet):
|
||||
|
||||
# Deactivate the user
|
||||
user.is_active = False
|
||||
user.last_workspace_id = None
|
||||
user.is_tour_completed = False
|
||||
user.is_onboarded = False
|
||||
user.onboarding_step = {
|
||||
|
||||
# Profile updates
|
||||
profile = Profile.objects.get(user=user)
|
||||
|
||||
profile.last_workspace_id = None
|
||||
profile.is_tour_completed = False
|
||||
profile.is_onboarded = False
|
||||
profile.onboarding_step = {
|
||||
"workspace_join": False,
|
||||
"profile_complete": False,
|
||||
"workspace_create": False,
|
||||
"workspace_invite": False,
|
||||
}
|
||||
profile.save()
|
||||
user.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
def patch(self, request):
|
||||
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||
user.save()
|
||||
profile = Profile.objects.get(user_id=request.user.id)
|
||||
profile.is_onboarded = request.data.get("is_onboarded", False)
|
||||
profile.save()
|
||||
return Response(
|
||||
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
@@ -156,9 +170,11 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
|
||||
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||
def patch(self, request):
|
||||
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
||||
user.save()
|
||||
profile = Profile.objects.get(user_id=request.user.id, is_active=True)
|
||||
profile.is_tour_completed = request.data.get(
|
||||
"is_tour_completed", False
|
||||
)
|
||||
profile.save()
|
||||
return Response(
|
||||
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
@@ -177,3 +193,18 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||
issue_activities, many=True
|
||||
).data,
|
||||
)
|
||||
|
||||
|
||||
class ProfileEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
profile = Profile.objects.get(user=request.user)
|
||||
serializer = ProfileSerializer(profile)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
accounts = Account.objects.filter(user=request.user)
|
||||
serializer = AccountSerializer(accounts, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -148,12 +148,10 @@ def send_email_notification(
|
||||
template_data = []
|
||||
total_changes = 0
|
||||
comments = []
|
||||
actors_involved = []
|
||||
for actor_id, changes in data.items():
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
total_changes = total_changes + len(changes)
|
||||
comment = changes.pop("comment", False)
|
||||
actors_involved.append(actor_id)
|
||||
if comment:
|
||||
comments.append(
|
||||
{
|
||||
@@ -186,14 +184,13 @@ def send_email_notification(
|
||||
}
|
||||
)
|
||||
|
||||
summary = "Updates were made to the issue by"
|
||||
summary = "updates were made to the issue by"
|
||||
|
||||
# Send the mail
|
||||
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
|
||||
context = {
|
||||
"data": template_data,
|
||||
"summary": summary,
|
||||
"actors_involved": len(set(actors_involved)),
|
||||
"issue": {
|
||||
"issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
|
||||
"name": issue.name,
|
||||
@@ -203,9 +200,6 @@ def send_email_notification(
|
||||
"email": receiver.email,
|
||||
},
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
||||
"project_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/",
|
||||
"workspace":str(issue.project.workspace.slug),
|
||||
"project": str(issue.project.name),
|
||||
"user_preference": f"{base_api}/profile/preferences/email",
|
||||
"comments": comments,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-26 12:35
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import plane.db.models.user
|
||||
|
||||
|
||||
def migrate_user_extra_profile(apps, schema_editor):
|
||||
Profile = apps.get_model("db", "Profile")
|
||||
User = apps.get_model("db", "User")
|
||||
|
||||
Profile.objects.bulk_create(
|
||||
[
|
||||
Profile(
|
||||
user_id=user.get("id"),
|
||||
theme=user.get("theme"),
|
||||
is_tour_completed=user.get("is_tour_completed"),
|
||||
use_case=user.get("use_case"),
|
||||
is_onboarded=user.get("is_onboarded"),
|
||||
last_workspace_id=user.get("last_workspace_id"),
|
||||
billing_address_country=user.get("billing_address_country"),
|
||||
billing_address=user.get("billing_address"),
|
||||
has_billing_address=user.get("has_billing_address"),
|
||||
)
|
||||
for user in User.objects.values(
|
||||
"id",
|
||||
"theme",
|
||||
"is_tour_completed",
|
||||
"onboarding_step",
|
||||
"use_case",
|
||||
"role",
|
||||
"is_onboarded",
|
||||
"last_workspace_id",
|
||||
"billing_address_country",
|
||||
"billing_address",
|
||||
"has_billing_address",
|
||||
)
|
||||
],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("db", "0058_alter_moduleissue_issue_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Profile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
("theme", models.JSONField(default=dict)),
|
||||
("is_tour_completed", models.BooleanField(default=False)),
|
||||
(
|
||||
"onboarding_step",
|
||||
models.JSONField(
|
||||
default=plane.db.models.user.get_default_onboarding
|
||||
),
|
||||
),
|
||||
("use_case", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"role",
|
||||
models.CharField(blank=True, max_length=300, null=True),
|
||||
),
|
||||
("is_onboarded", models.BooleanField(default=False)),
|
||||
("last_workspace_id", models.UUIDField(null=True)),
|
||||
(
|
||||
"billing_address_country",
|
||||
models.CharField(default="INDIA", max_length=255),
|
||||
),
|
||||
("billing_address", models.JSONField(null=True)),
|
||||
("has_billing_address", models.BooleanField(default=False)),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="profile",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Profile",
|
||||
"verbose_name_plural": "Profiles",
|
||||
"db_table": "profiles",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(migrate_user_extra_profile),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="billing_address",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="billing_address_country",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="has_billing_address",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_onboarded",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_tour_completed",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="last_workspace_id",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="my_issues_prop",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="onboarding_step",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="role",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="theme",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="use_case",
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Account",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
("provider_account_id", models.CharField(max_length=255)),
|
||||
(
|
||||
"provider",
|
||||
models.CharField(
|
||||
choices=[("google", "Google"), ("github", "Github")]
|
||||
),
|
||||
),
|
||||
("access_token", models.TextField()),
|
||||
("access_token_expired_at", models.DateTimeField(null=True)),
|
||||
("refresh_token", models.TextField(blank=True, null=True)),
|
||||
("refresh_token_expired_at", models.DateTimeField(null=True)),
|
||||
(
|
||||
"last_connected_at",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("metadata", models.JSONField(default=dict)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="accounts",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Account",
|
||||
"verbose_name_plural": "Accounts",
|
||||
"db_table": "accounts",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
from .base import BaseModel
|
||||
|
||||
from .user import User
|
||||
from .user import User, Profile, Account
|
||||
|
||||
from .workspace import (
|
||||
Workspace,
|
||||
|
||||
@@ -21,6 +21,9 @@ from sentry_sdk import capture_exception
|
||||
from slack_sdk import WebClient
|
||||
from slack_sdk.errors import SlackApiError
|
||||
|
||||
# Module imports
|
||||
from ..mixins import TimeAuditModel
|
||||
|
||||
|
||||
def get_default_onboarding():
|
||||
return {
|
||||
@@ -40,12 +43,14 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
primary_key=True,
|
||||
)
|
||||
username = models.CharField(max_length=128, unique=True)
|
||||
|
||||
# user fields
|
||||
mobile_number = models.CharField(max_length=255, blank=True, null=True)
|
||||
email = models.CharField(
|
||||
max_length=255, null=True, blank=True, unique=True
|
||||
)
|
||||
|
||||
# identity
|
||||
display_name = models.CharField(max_length=255, default="")
|
||||
first_name = models.CharField(max_length=255, blank=True)
|
||||
last_name = models.CharField(max_length=255, blank=True)
|
||||
avatar = models.CharField(max_length=255, blank=True)
|
||||
@@ -72,19 +77,10 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
is_staff = models.BooleanField(default=False)
|
||||
is_email_verified = models.BooleanField(default=False)
|
||||
is_password_autoset = models.BooleanField(default=False)
|
||||
is_onboarded = models.BooleanField(default=False)
|
||||
|
||||
# random token generated
|
||||
token = models.CharField(max_length=64, blank=True)
|
||||
|
||||
billing_address_country = models.CharField(max_length=255, default="INDIA")
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
|
||||
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
user_timezone = models.CharField(
|
||||
max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES
|
||||
)
|
||||
|
||||
last_active = models.DateTimeField(default=timezone.now, null=True)
|
||||
last_login_time = models.DateTimeField(null=True)
|
||||
last_logout_time = models.DateTimeField(null=True)
|
||||
@@ -96,18 +92,17 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
)
|
||||
last_login_uagent = models.TextField(blank=True)
|
||||
token_updated_at = models.DateTimeField(null=True)
|
||||
last_workspace_id = models.UUIDField(null=True)
|
||||
my_issues_prop = models.JSONField(null=True)
|
||||
role = models.CharField(max_length=300, null=True, blank=True)
|
||||
# my_issues_prop = models.JSONField(null=True)
|
||||
|
||||
is_bot = models.BooleanField(default=False)
|
||||
theme = models.JSONField(default=dict)
|
||||
display_name = models.CharField(max_length=255, default="")
|
||||
is_tour_completed = models.BooleanField(default=False)
|
||||
onboarding_step = models.JSONField(default=get_default_onboarding)
|
||||
use_case = models.TextField(blank=True, null=True)
|
||||
|
||||
# timezone
|
||||
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
user_timezone = models.CharField(
|
||||
max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES
|
||||
)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
|
||||
REQUIRED_FIELDS = ["username"]
|
||||
|
||||
objects = UserManager()
|
||||
@@ -144,6 +139,60 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
super(User, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class Profile(TimeAuditModel):
|
||||
# User
|
||||
user = models.OneToOneField("db.User", on_delete=models.CASCADE, related_name="profile")
|
||||
|
||||
# General
|
||||
theme = models.JSONField(default=dict)
|
||||
|
||||
# Onboarding
|
||||
is_tour_completed = models.BooleanField(default=False)
|
||||
onboarding_step = models.JSONField(default=get_default_onboarding)
|
||||
use_case = models.TextField(blank=True, null=True)
|
||||
role = models.CharField(max_length=300, null=True, blank=True) # job role
|
||||
is_onboarded = models.BooleanField(default=False)
|
||||
|
||||
# Last visited workspace
|
||||
last_workspace_id = models.UUIDField(null=True)
|
||||
|
||||
# address data
|
||||
billing_address_country = models.CharField(max_length=255, default="INDIA")
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Profile"
|
||||
verbose_name_plural = "Profiles"
|
||||
db_table = "profiles"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
|
||||
class Account(TimeAuditModel):
|
||||
user = models.ForeignKey(
|
||||
"db.User", on_delete=models.CASCADE, related_name="accounts"
|
||||
)
|
||||
provider_account_id = models.CharField(max_length=255)
|
||||
provider = models.CharField(
|
||||
choices=(("google", "Google"), ("github", "Github"))
|
||||
)
|
||||
access_token = models.TextField()
|
||||
access_token_expired_at = models.DateTimeField(null=True)
|
||||
refresh_token = models.TextField(null=True, blank=True)
|
||||
refresh_token_expired_at = models.DateTimeField(null=True)
|
||||
last_connected_at = models.DateTimeField(default=timezone.now)
|
||||
metadata = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Account"
|
||||
verbose_name_plural = "Accounts"
|
||||
db_table = "accounts"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def send_welcome_slack(sender, instance, created, **kwargs):
|
||||
try:
|
||||
@@ -170,6 +219,7 @@ def create_user_notification(sender, instance, created, **kwargs):
|
||||
if created and not instance.is_bot:
|
||||
# Module imports
|
||||
from plane.db.models import UserNotificationPreference
|
||||
|
||||
UserNotificationPreference.objects.create(
|
||||
user=instance,
|
||||
)
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,109 @@
|
||||
import { TextSelection } from "prosemirror-state";
|
||||
|
||||
import { InputRule, mergeAttributes, Node, nodeInputRule, wrappingInputRule } from "@tiptap/core";
|
||||
|
||||
/**
|
||||
* Extension based on:
|
||||
* - Tiptap HorizontalRule extension (https://tiptap.dev/api/nodes/horizontal-rule)
|
||||
*/
|
||||
|
||||
export interface HorizontalRuleOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
horizontalRule: {
|
||||
/**
|
||||
* Add a horizontal rule
|
||||
*/
|
||||
setHorizontalRule: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const HorizontalRule = Node.create<HorizontalRuleOptions>({
|
||||
name: "horizontalRule",
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
};
|
||||
},
|
||||
|
||||
group: "block",
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
color: {
|
||||
default: "#dddddd",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `div[data-type="${this.name}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"div",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||
"data-type": this.name,
|
||||
}),
|
||||
["div", {}],
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setHorizontalRule:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
return (
|
||||
chain()
|
||||
.insertContent({ type: this.name })
|
||||
// set cursor after horizontal rule
|
||||
.command(({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
const { $to } = tr.selection;
|
||||
const posAfter = $to.end();
|
||||
|
||||
if ($to.nodeAfter) {
|
||||
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
|
||||
} else {
|
||||
// add node after horizontal rule if it’s the end of the document
|
||||
const node = $to.parent.type.contentMatch.defaultType?.create();
|
||||
|
||||
if (node) {
|
||||
tr.insert(posAfter, node);
|
||||
tr.setSelection(TextSelection.create(tr.doc, posAfter));
|
||||
}
|
||||
}
|
||||
|
||||
tr.scrollIntoView();
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.run()
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
new InputRule({
|
||||
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||
handler: ({ state, range, match }) => {
|
||||
state.tr.replaceRangeWith(range.from, range.to, this.type.create());
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -1,25 +1,26 @@
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import TiptapUnderline from "@tiptap/extension-underline";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import TaskItem from "@tiptap/extension-task-item";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import TiptapUnderline from "@tiptap/extension-underline";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
|
||||
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
|
||||
import { Table } from "src/ui/extensions/table/table";
|
||||
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
|
||||
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
|
||||
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
|
||||
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
|
||||
|
||||
import { ImageExtension } from "src/ui/extensions/image";
|
||||
|
||||
import { isValidHttpUrl } from "src/lib/utils";
|
||||
import { Mentions } from "src/ui/mentions";
|
||||
|
||||
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
|
||||
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
|
||||
import { CustomKeymap } from "src/ui/extensions/keymap";
|
||||
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
|
||||
import { CustomQuoteExtension } from "src/ui/extensions/quote";
|
||||
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
|
||||
|
||||
import { DeleteImage } from "src/types/delete-image";
|
||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
@@ -54,9 +55,7 @@ export const CoreEditorExtensions = (
|
||||
},
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: {
|
||||
HTMLAttributes: { class: "mt-4 mb-4" },
|
||||
},
|
||||
horizontalRule: false,
|
||||
blockquote: false,
|
||||
dropcursor: {
|
||||
color: "rgba(var(--color-text-100))",
|
||||
@@ -105,6 +104,7 @@ export const CoreEditorExtensions = (
|
||||
transformCopiedText: true,
|
||||
transformPastedText: true,
|
||||
}),
|
||||
HorizontalRule,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
|
||||
@@ -10,11 +10,6 @@ export interface CustomMentionOptions extends MentionOptions {
|
||||
}
|
||||
|
||||
export const CustomMention = Mention.extend<CustomMentionOptions>({
|
||||
addStorage(this) {
|
||||
return {
|
||||
mentionsOpen: false,
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
id: {
|
||||
|
||||
@@ -14,7 +14,6 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
||||
|
||||
return {
|
||||
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||
props.editor.storage.mentionsOpen = true;
|
||||
reactRenderer = new ReactRenderer(MentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
@@ -46,18 +45,10 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
||||
return true;
|
||||
}
|
||||
|
||||
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
|
||||
|
||||
if (navigationKeys.includes(props.event.key)) {
|
||||
// @ts-ignore
|
||||
reactRenderer?.ref?.onKeyDown(props);
|
||||
event?.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// @ts-ignore
|
||||
return reactRenderer?.ref?.onKeyDown(props);
|
||||
},
|
||||
onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
|
||||
props.editor.storage.mentionsOpen = false;
|
||||
onExit: () => {
|
||||
popup?.[0].destroy();
|
||||
reactRenderer?.destroy();
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
|
||||
import { Table } from "src/ui/extensions/table/table";
|
||||
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
|
||||
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
|
||||
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
|
||||
|
||||
import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image";
|
||||
import { isValidHttpUrl } from "src/lib/utils";
|
||||
@@ -50,9 +51,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
},
|
||||
},
|
||||
codeBlock: false,
|
||||
horizontalRule: {
|
||||
HTMLAttributes: { class: "mt-4 mb-4" },
|
||||
},
|
||||
horizontalRule: false,
|
||||
dropcursor: {
|
||||
color: "rgba(var(--color-text-100))",
|
||||
width: 2,
|
||||
@@ -73,6 +72,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
class: "rounded-lg border border-custom-border-300",
|
||||
},
|
||||
}),
|
||||
HorizontalRule,
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
Color,
|
||||
|
||||
@@ -4,16 +4,13 @@ export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
|
||||
Extension.create({
|
||||
name: "enterKey",
|
||||
|
||||
addKeyboardShortcuts(this) {
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Enter: () => {
|
||||
if (!this.editor.storage.mentionsOpen) {
|
||||
if (onEnterKeyPress) {
|
||||
onEnterKeyPress();
|
||||
}
|
||||
return true;
|
||||
if (onEnterKeyPress) {
|
||||
onEnterKeyPress();
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
"Shift-Enter": ({ editor }) =>
|
||||
editor.commands.first(({ commands }) => [
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension";
|
||||
|
||||
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [EnterKeyExtension(onEnterKeyPress)];
|
||||
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [
|
||||
// EnterKeyExtension(onEnterKeyPress),
|
||||
];
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { APIService } from "../api.service";
|
||||
// types
|
||||
import { GptApiResponse } from "@plane/types";
|
||||
|
||||
export class AIService extends APIService {
|
||||
constructor(BASE_URL: string) {
|
||||
super(BASE_URL);
|
||||
}
|
||||
|
||||
async createGptTask(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: { prompt: string; task: string }
|
||||
): Promise<GptApiResponse> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import {
|
||||
IAnalyticsParams,
|
||||
IAnalyticsResponse,
|
||||
IDefaultAnalyticsResponse,
|
||||
IExportAnalyticsFormData,
|
||||
ISaveAnalyticsFormData,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class AnalyticsService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise<IAnalyticsResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/analytics/`, {
|
||||
params: {
|
||||
...params,
|
||||
project: params?.project ? params.project.toString() : null,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getDefaultAnalytics(
|
||||
workspaceSlug: string,
|
||||
params?: Partial<IAnalyticsParams>
|
||||
): Promise<IDefaultAnalyticsResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/default-analytics/`, {
|
||||
params: {
|
||||
...params,
|
||||
project: params?.project ? params.project.toString() : null,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async saveAnalytics(workspaceSlug: string, data: ISaveAnalyticsFormData): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/analytic-view/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async exportAnalytics(workspaceSlug: string, data: IExportAnalyticsFormData): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/export-analytics/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import axios from "axios";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
export abstract class APIService {
|
||||
protected baseURL: string;
|
||||
protected headers: any = {};
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
setRefreshToken(token: string) {
|
||||
Cookies.set("refreshToken", token, { expires: 30 });
|
||||
}
|
||||
|
||||
getRefreshToken() {
|
||||
return Cookies.get("refreshToken");
|
||||
}
|
||||
|
||||
purgeRefreshToken() {
|
||||
Cookies.remove("refreshToken", { path: "/" });
|
||||
}
|
||||
|
||||
setAccessToken(token: string) {
|
||||
Cookies.set("accessToken", token, { expires: 30 });
|
||||
}
|
||||
|
||||
getAccessToken() {
|
||||
return Cookies.get("accessToken");
|
||||
}
|
||||
|
||||
purgeAccessToken() {
|
||||
Cookies.remove("accessToken", { path: "/" });
|
||||
}
|
||||
|
||||
getHeaders() {
|
||||
return {
|
||||
Authorization: `Bearer ${this.getAccessToken()}`,
|
||||
};
|
||||
}
|
||||
|
||||
get(url: string, config = {}): Promise<any> {
|
||||
return axios({
|
||||
method: "get",
|
||||
url: this.baseURL + url,
|
||||
headers: this.getAccessToken() ? this.getHeaders() : {},
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
post(url: string, data = {}, config = {}): Promise<any> {
|
||||
return axios({
|
||||
method: "post",
|
||||
url: this.baseURL + url,
|
||||
data,
|
||||
headers: this.getAccessToken() ? this.getHeaders() : {},
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
put(url: string, data = {}, config = {}): Promise<any> {
|
||||
return axios({
|
||||
method: "put",
|
||||
url: this.baseURL + url,
|
||||
data,
|
||||
headers: this.getAccessToken() ? this.getHeaders() : {},
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
patch(url: string, data = {}, config = {}): Promise<any> {
|
||||
return axios({
|
||||
method: "patch",
|
||||
url: this.baseURL + url,
|
||||
data,
|
||||
headers: this.getAccessToken() ? this.getHeaders() : {},
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
delete(url: string, data?: any, config = {}): Promise<any> {
|
||||
return axios({
|
||||
method: "delete",
|
||||
url: this.baseURL + url,
|
||||
data: data,
|
||||
headers: this.getAccessToken() ? this.getHeaders() : {},
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
request(config = {}) {
|
||||
return axios(config);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { AIService } from "./ai/ai.service";
|
||||
import { WorkspaceService } from "./workspace/workspace.service";
|
||||
|
||||
export class Client {
|
||||
ai;
|
||||
workspace;
|
||||
|
||||
constructor(BASE_URL: string | undefined) {
|
||||
this.user = new UserService(BASE_URL || "");
|
||||
this.instance = new InstanceService(BASE_URL || "");
|
||||
|
||||
this.ai = new AIService(BASE_URL || "");
|
||||
this.workspace = new WorkspaceService(BASE_URL || "");
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { CycleDateCheckData, ICycle, TIssue, TIssueMap } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class CycleService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<ICycle> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getCyclesWithParams(workspaceSlug: string, projectId: string, cycleType?: "current"): Promise<ICycle[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, {
|
||||
params: {
|
||||
cycle_view: cycleType,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getCycleDetails(workspaceSlug: string, projectId: string, cycleId: string): Promise<ICycle> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`)
|
||||
.then((res) => res?.data)
|
||||
.catch((err) => {
|
||||
throw err?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getCycleIssues(workspaceSlug: string, projectId: string, cycleId: string): Promise<TIssue[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getCycleIssuesWithParams(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
queries?: any
|
||||
): Promise<TIssue[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchCycle(workspaceSlug: string, projectId: string, cycleId: string, data: Partial<ICycle>): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCycle(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async cycleDateCheck(workspaceSlug: string, projectId: string, data: CycleDateCheckData): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addCycleToFavorites(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: {
|
||||
cycle: string;
|
||||
}
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async transferIssues(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
data: {
|
||||
new_cycle_id: string;
|
||||
}
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/transfer-issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeCycleFromFavorites(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/${cycleId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { TInboxIssueFilterOptions, TInboxIssueExtendedDetail, TIssue, TInboxDetailedStatus } from "@plane/types";
|
||||
|
||||
export class InboxIssueService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetchInboxIssues(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
params?: TInboxIssueFilterOptions | {}
|
||||
): Promise<TInboxIssueExtendedDetail[]> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/?expand=issue_inbox`,
|
||||
{
|
||||
params,
|
||||
}
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchInboxIssueById(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string
|
||||
): Promise<TInboxIssueExtendedDetail> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/?expand=issue_inbox`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
data: {
|
||||
source: string;
|
||||
issue: Partial<TIssue>;
|
||||
}
|
||||
): Promise<TInboxIssueExtendedDetail> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/?expand=issue_inbox`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: { issue: Partial<TIssue> }
|
||||
): Promise<TInboxIssueExtendedDetail> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/?expand=issue_inbox`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string
|
||||
): Promise<void> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInboxIssueStatus(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: TInboxDetailedStatus
|
||||
): Promise<TInboxIssueExtendedDetail> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/?expand=issue_inbox`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { IInboxIssue, IInbox, TInboxStatus, IInboxQueryParams } from "@plane/types";
|
||||
|
||||
export class InboxService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getInboxes(workspaceSlug: string, projectId: string): Promise<IInbox[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getInboxById(workspaceSlug: string, projectId: string, inboxId: string): Promise<IInbox> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchInbox(workspaceSlug: string, projectId: string, inboxId: string, data: Partial<IInbox>): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getInboxIssues(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
params?: IInboxQueryParams
|
||||
): Promise<IInboxIssue[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getInboxIssueById(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string
|
||||
): Promise<IInboxIssue> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async markInboxStatus(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: TInboxStatus
|
||||
): Promise<IInboxIssue> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: { issue: Partial<IInboxIssue> }
|
||||
): Promise<any> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createInboxIssue(workspaceSlug: string, projectId: string, inboxId: string, data: any): Promise<IInboxIssue> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { TInbox } from "@plane/types";
|
||||
|
||||
export class InboxService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetchInboxes(workspaceSlug: string, projectId: string): Promise<TInbox[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchInboxById(workspaceSlug: string, projectId: string, inboxId: string): Promise<TInbox> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInbox(workspaceSlug: string, projectId: string, inboxId: string, data: Partial<TInbox>): Promise<TInbox> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./inbox.service";
|
||||
export * from "./inbox-issue.service";
|
||||
@@ -1,49 +0,0 @@
|
||||
import { APIService } from "../api.service";
|
||||
import { IApiToken } from "@plane/types";
|
||||
|
||||
export class APITokenService extends APIService {
|
||||
constructor(BASE_URL) {
|
||||
super(BASE_URL);
|
||||
}
|
||||
|
||||
async getApiTokens(workspaceSlug: string): Promise<IApiToken[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveApiToken(
|
||||
workspaceSlug: string,
|
||||
tokenId: String
|
||||
): Promise<IApiToken> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createApiToken(
|
||||
workspaceSlug: string,
|
||||
data: Partial<IApiToken>
|
||||
): Promise<IApiToken> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/api-tokens/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteApiToken(
|
||||
workspaceSlug: string,
|
||||
tokenId: String
|
||||
): Promise<IApiToken> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// helper
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import { IAppConfig } from "@plane/types";
|
||||
|
||||
export class AppConfigService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async envConfig(): Promise<IAppConfig> {
|
||||
return this.get("/api/configs/", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
import axios from "axios";
|
||||
|
||||
export interface UnSplashImage {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
promoted_at: Date;
|
||||
width: number;
|
||||
height: number;
|
||||
color: string;
|
||||
blur_hash: string;
|
||||
description: null;
|
||||
alt_description: string;
|
||||
urls: UnSplashImageUrls;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface UnSplashImageUrls {
|
||||
raw: string;
|
||||
full: string;
|
||||
regular: string;
|
||||
small: string;
|
||||
thumb: string;
|
||||
small_s3: string;
|
||||
}
|
||||
|
||||
export class FileService extends APIService {
|
||||
private cancelSource: any;
|
||||
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
this.uploadFile = this.uploadFile.bind(this);
|
||||
this.deleteImage = this.deleteImage.bind(this);
|
||||
this.restoreImage = this.restoreImage.bind(this);
|
||||
this.cancelUpload = this.cancelUpload.bind(this);
|
||||
}
|
||||
|
||||
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
||||
this.cancelSource = axios.CancelToken.source();
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, {
|
||||
headers: {
|
||||
...this.getHeaders(),
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
cancelToken: this.cancelSource.token,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
if (axios.isCancel(error)) {
|
||||
console.log(error.message);
|
||||
} else {
|
||||
console.log(error);
|
||||
throw error?.response?.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cancelUpload() {
|
||||
this.cancelSource.cancel("Upload cancelled");
|
||||
}
|
||||
|
||||
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
||||
return async (file: File) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("asset", file);
|
||||
formData.append("attributes", JSON.stringify({}));
|
||||
|
||||
const data = await this.uploadFile(workspaceSlug, formData);
|
||||
return data.asset;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getDeleteImageFunction(workspaceId: string) {
|
||||
return async (src: string) => {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = `${workspaceId}/${this.extractAssetIdFromUrl(src, workspaceId)}`;
|
||||
const data = await this.deleteImage(assetUrlWithWorkspaceId);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getRestoreImageFunction(workspaceId: string) {
|
||||
return async (src: string) => {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = `${workspaceId}/${this.extractAssetIdFromUrl(src, workspaceId)}`;
|
||||
const data = await this.restoreImage(assetUrlWithWorkspaceId);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
extractAssetIdFromUrl(src: string, workspaceId: string): string {
|
||||
const indexWhereAssetIdStarts = src.indexOf(workspaceId) + workspaceId.length + 1;
|
||||
if (indexWhereAssetIdStarts === -1) {
|
||||
throw new Error("Workspace ID not found in source string");
|
||||
}
|
||||
const assetUrl = src.substring(indexWhereAssetIdStarts);
|
||||
return assetUrl;
|
||||
}
|
||||
|
||||
async deleteImage(assetUrlWithWorkspaceId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`)
|
||||
.then((response) => response?.status)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async restoreImage(assetUrlWithWorkspaceId: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/restore/`, {
|
||||
headers: this.getHeaders(),
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
.then((response) => response?.status)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteFile(workspaceId: string, assetUrl: string): Promise<any> {
|
||||
const lastIndex = assetUrl.lastIndexOf("/");
|
||||
const assetId = assetUrl.substring(lastIndex + 1);
|
||||
|
||||
return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async uploadUserFile(file: FormData): Promise<any> {
|
||||
return this.post(`/api/users/file-assets/`, file, {
|
||||
headers: {
|
||||
...this.getHeaders(),
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserFile(assetUrl: string): Promise<any> {
|
||||
const lastIndex = assetUrl.lastIndexOf("/");
|
||||
const assetId = assetUrl.substring(lastIndex + 1);
|
||||
|
||||
return this.delete(`/api/users/file-assets/${assetId}`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUnsplashImages(query?: string): Promise<UnSplashImage[]> {
|
||||
return this.get(`/api/unsplash/`, {
|
||||
params: {
|
||||
query,
|
||||
},
|
||||
})
|
||||
.then((res) => res?.data?.results ?? res?.data)
|
||||
.catch((err) => {
|
||||
throw err?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectCoverImages(): Promise<string[]> {
|
||||
return this.get(`/api/project-covers/`)
|
||||
.then((res) => res?.data)
|
||||
.catch((err) => {
|
||||
throw err?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types";
|
||||
|
||||
export class InstanceService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getInstanceInfo(): Promise<IInstance> {
|
||||
return this.get("/api/instances/", { headers: {} })
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async getInstanceAdmins(): Promise<IInstanceAdmin[]> {
|
||||
return this.get("/api/instances/admins/")
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInstanceInfo(data: Partial<IInstance>): Promise<IInstance> {
|
||||
return this.patch("/api/instances/", data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getInstanceConfigurations() {
|
||||
return this.get("/api/instances/configurations/")
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInstanceConfigurations(
|
||||
data: Partial<IFormattedInstanceConfiguration>
|
||||
): Promise<IInstanceConfiguration[]> {
|
||||
return this.patch("/api/instances/configurations/", data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// api services
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import { IWebhook } from "@plane/types";
|
||||
|
||||
export class WebhookService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetchWebhooksList(workspaceSlug: string): Promise<IWebhook[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/webhooks/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchWebhookDetails(workspaceSlug: string, webhookId: string): Promise<IWebhook> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/webhooks/${webhookId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createWebhook(workspaceSlug: string, data: {}): Promise<IWebhook> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/webhooks/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateWebhook(workspaceSlug: string, webhookId: string, data: {}): Promise<IWebhook> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/webhooks/${webhookId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteWebhook(workspaceSlug: string, webhookId: string): Promise<void> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/webhooks/${webhookId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async regenerateSecretKey(workspaceSlug: string, webhookId: string): Promise<IWebhook> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/webhooks/${webhookId}/regenerate/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import { IGithubRepoInfo, IGithubServiceImportFormData } from "@plane/types";
|
||||
|
||||
const integrationServiceType: string = "github";
|
||||
|
||||
export class GithubIntegrationService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async listAllRepositories(workspaceSlug: string, integrationSlug: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/workspace-integrations/${integrationSlug}/github-repositories`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getGithubRepoInfo(workspaceSlug: string, params: { owner: string; repo: string }): Promise<IGithubRepoInfo> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/importers/${integrationServiceType}/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createGithubServiceImport(workspaceSlug: string, data: IGithubServiceImportFormData): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/${integrationServiceType}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./github.service";
|
||||
export * from "./integration.service";
|
||||
export * from "./jira.service";
|
||||
@@ -1,67 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { IAppIntegration, IImporterService, IWorkspaceIntegration, IExportServiceResponse } from "@plane/types";
|
||||
// helper
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class IntegrationService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getAppIntegrationsList(): Promise<IAppIntegration[]> {
|
||||
return this.get(`/api/integrations/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceIntegrationsList(workspaceSlug: string): Promise<IWorkspaceIntegration[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/workspace-integrations/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteWorkspaceIntegration(workspaceSlug: string, integrationId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/workspace-integrations/${integrationId}/provider/`)
|
||||
.then((res) => res?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getImporterServicesList(workspaceSlug: string): Promise<IImporterService[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/importers/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
async getExportsServicesList(
|
||||
workspaceSlug: string,
|
||||
cursor: string,
|
||||
per_page: number
|
||||
): Promise<IExportServiceResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/export-issues`, {
|
||||
params: {
|
||||
per_page,
|
||||
cursor,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteImporterService(workspaceSlug: string, service: string, importerId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "@plane/types";
|
||||
|
||||
export class JiraImporterService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getJiraProjectInfo(workspaceSlug: string, params: IJiraMetadata): Promise<IJiraResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/importers/jira`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createJiraImporter(workspaceSlug: string, data: IJiraImporterForm): Promise<IJiraResponse> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export * from "./issue_archive.service";
|
||||
export * from "./issue.service";
|
||||
export * from "./issue_draft.service";
|
||||
export * from "./issue_reaction.service";
|
||||
export * from "./issue_label.service";
|
||||
export * from "./issue_attachment.service";
|
||||
export * from "./issue_activity.service";
|
||||
export * from "./issue_comment.service";
|
||||
export * from "./issue_relation.service";
|
||||
@@ -1,238 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// type
|
||||
import type {
|
||||
TIssue,
|
||||
IIssueDisplayProperties,
|
||||
ILinkDetails,
|
||||
TIssueLink,
|
||||
TIssueSubIssues,
|
||||
TIssueActivity,
|
||||
} from "@plane/types";
|
||||
// helper
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class IssueService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async createIssue(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getIssues(workspaceSlug: string, projectId: string, queries?: any): Promise<TIssue[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getIssuesWithParams(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
queries?: any
|
||||
): Promise<TIssue[] | { [key: string]: TIssue[] }> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieve(workspaceSlug: string, projectId: string, issueId: string, queries?: any): Promise<TIssue> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getIssueActivities(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueActivity[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addIssueToCycle(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
data: {
|
||||
issues: string[];
|
||||
}
|
||||
) {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeIssueFromCycle(workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/${bridgeId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createIssueRelation(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: {
|
||||
related_list: Array<{
|
||||
relation_type: "duplicate" | "relates_to" | "blocked_by";
|
||||
related_issue: string;
|
||||
}>;
|
||||
relation?: "blocking" | null;
|
||||
}
|
||||
) {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueRelation(workspaceSlug: string, projectId: string, issueId: string, relationId: string) {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/${relationId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async getIssueDisplayProperties(workspaceSlug: string, projectId: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-display-properties/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateIssueDisplayProperties(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssueDisplayProperties
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-display-properties/`, {
|
||||
properties: data,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchIssue(workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async bulkDeleteIssues(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-delete-issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async subIssues(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueSubIssues> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addSubIssues(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: { sub_issue_ids: string[] }
|
||||
): Promise<TIssueSubIssues> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchIssueLinks(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueLink[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async createIssueLink(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: Partial<TIssueLink>
|
||||
): Promise<ILinkDetails> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async updateIssueLink(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
linkId: string,
|
||||
data: Partial<TIssueLink>
|
||||
): Promise<ILinkDetails> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueLink(workspaceSlug: string, projectId: string, issueId: string, linkId: string): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { TIssueActivity } from "@plane/types";
|
||||
// helper
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class IssueActivityService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getIssueActivities(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
params:
|
||||
| {
|
||||
created_at__gt: string;
|
||||
}
|
||||
| {} = {}
|
||||
): Promise<TIssueActivity[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`, {
|
||||
params: {
|
||||
activity_type: "issue-property",
|
||||
...params,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// type
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class IssueArchiveService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getArchivedIssues(workspaceSlug: string, projectId: string, queries?: any): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/`, {
|
||||
params: { ...queries },
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async unarchiveIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/unarchive/${issueId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveArchivedIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issueId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteArchivedIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issuesId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helper
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import { TIssueAttachment } from "@plane/types";
|
||||
|
||||
export class IssueAttachmentService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async uploadIssueAttachment(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
file: FormData
|
||||
): Promise<TIssueAttachment> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-attachments/`,
|
||||
file,
|
||||
{
|
||||
headers: {
|
||||
...this.getHeaders(),
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getIssueAttachment(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueAttachment[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-attachments/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueAttachment(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
assetId: string
|
||||
): Promise<TIssueAttachment> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-attachments/${assetId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { TIssueComment } from "@plane/types";
|
||||
// helper
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class IssueCommentService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getIssueComments(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
params:
|
||||
| {
|
||||
created_at__gt: string;
|
||||
}
|
||||
| {} = {}
|
||||
): Promise<TIssueComment[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`, {
|
||||
params: {
|
||||
activity_type: "issue-comment",
|
||||
...params,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createIssueComment(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: Partial<TIssueComment>
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchIssueComment(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
data: Partial<TIssueComment>
|
||||
): Promise<any> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueComment(workspaceSlug: string, projectId: string, issueId: string, commentId: string): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
export class IssueDraftService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getDraftIssues(workspaceSlug: string, projectId: string, query?: any): Promise<TIssue[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, {
|
||||
params: { ...query },
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createDraftIssue(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async updateDraftIssue(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteDraftIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { IIssueFiltersResponse } from "@plane/types";
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class IssueFiltersService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
// // workspace issue filters
|
||||
// async fetchWorkspaceFilters(workspaceSlug: string): Promise<IIssueFiltersResponse> {
|
||||
// return this.get(`/api/workspaces/${workspaceSlug}/user-properties/`)
|
||||
// .then((response) => response?.data)
|
||||
// .catch((error) => {
|
||||
// throw error?.response?.data;
|
||||
// });
|
||||
// }
|
||||
// async patchWorkspaceFilters(
|
||||
// workspaceSlug: string,
|
||||
// data: Partial<IIssueFiltersResponse>
|
||||
// ): Promise<IIssueFiltersResponse> {
|
||||
// return this.patch(`/api/workspaces/${workspaceSlug}/user-properties/`, data)
|
||||
// .then((response) => response?.data)
|
||||
// .catch((error) => {
|
||||
// throw error?.response?.data;
|
||||
// });
|
||||
// }
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
// cycle issue filters
|
||||
async fetchCycleIssueFilters(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string
|
||||
): Promise<IIssueFiltersResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/user-properties/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
async patchCycleIssueFilters(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
data: Partial<IIssueFiltersResponse>
|
||||
): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/user-properties/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
// module issue filters
|
||||
async fetchModuleIssueFilters(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string
|
||||
): Promise<IIssueFiltersResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/user-properties/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
async patchModuleIssueFilters(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
data: Partial<IIssueFiltersResponse>
|
||||
): Promise<any> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/user-properties/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { IIssueLabel } from "@plane/types";
|
||||
|
||||
export class IssueLabelService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getWorkspaceIssueLabels(workspaceSlug: string): Promise<IIssueLabel[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/labels/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectLabels(workspaceSlug: string, projectId: string): Promise<IIssueLabel[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createIssueLabel(workspaceSlug: string, projectId: string, data: any): Promise<IIssueLabel> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchIssueLabel(workspaceSlug: string, projectId: string, labelId: string, data: any): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueLabel(workspaceSlug: string, projectId: string, labelId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { TIssueCommentReaction, TIssueReaction } from "@plane/types";
|
||||
|
||||
export class IssueReactionService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async createIssueReaction(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: Partial<TIssueReaction>
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async listIssueReactions(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueReaction[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueReaction(workspaceSlug: string, projectId: string, issueId: string, reaction: string): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/${reaction}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createIssueCommentReaction(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
data: Partial<TIssueCommentReaction>
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async listIssueCommentReactions(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string
|
||||
): Promise<TIssueCommentReaction[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueCommentReaction(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reaction: string
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/${reaction}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { TIssueRelation, TIssue, TIssueRelationTypes } from "@plane/types";
|
||||
|
||||
export class IssueRelationService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async listIssueRelations(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueRelation> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createIssueRelations(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: { relation_type: TIssueRelationTypes; issues: string[] }
|
||||
): Promise<TIssue[]> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueRelation(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: { relation_type: TIssueRelationTypes; related_issue: string }
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/remove-relation/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { IModule, TIssue, ILinkDetails, ModuleLink } from "@plane/types";
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class ModuleService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getModules(workspaceSlug: string, projectId: string): Promise<IModule[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createModule(workspaceSlug: string, projectId: string, data: any): Promise<IModule> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateModule(workspaceSlug: string, projectId: string, moduleId: string, data: any): Promise<any> {
|
||||
return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getModuleDetails(workspaceSlug: string, projectId: string, moduleId: string): Promise<IModule> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchModule(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
data: Partial<IModule>
|
||||
): Promise<IModule> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteModule(workspaceSlug: string, projectId: string, moduleId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getModuleIssues(workspaceSlug: string, projectId: string, moduleId: string, queries?: any): Promise<TIssue[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addIssuesToModule(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
data: { issues: string[] }
|
||||
): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addModulesToIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: { modules: string[] }
|
||||
): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/modules/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeIssueFromModule(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
issueId: string
|
||||
): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/${issueId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeIssuesFromModuleBulk(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
issueIds: string[]
|
||||
): Promise<any> {
|
||||
const promiseDataUrls: any = [];
|
||||
issueIds.forEach((issueId) => {
|
||||
promiseDataUrls.push(
|
||||
this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/${issueId}/`)
|
||||
);
|
||||
});
|
||||
return await Promise.all(promiseDataUrls)
|
||||
.then((response) => response)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeModulesFromIssueBulk(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
moduleIds: string[]
|
||||
): Promise<any> {
|
||||
const promiseDataUrls: any = [];
|
||||
moduleIds.forEach((moduleId) => {
|
||||
promiseDataUrls.push(
|
||||
this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/${issueId}/`)
|
||||
);
|
||||
});
|
||||
return await Promise.all(promiseDataUrls)
|
||||
.then((response) => response)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createModuleLink(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
data: Partial<ModuleLink>
|
||||
): Promise<ILinkDetails> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async updateModuleLink(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
linkId: string,
|
||||
data: Partial<ModuleLink>
|
||||
): Promise<ILinkDetails> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/${linkId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteModuleLink(workspaceSlug: string, projectId: string, moduleId: string, linkId: string): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/${linkId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addModuleToFavorites(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: {
|
||||
module: string;
|
||||
}
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeModuleFromFavorites(workspaceSlug: string, projectId: string, moduleId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/${moduleId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { IPage, IPageBlock, TIssue } from "@plane/types";
|
||||
|
||||
export class PageService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async createPage(workspaceSlug: string, projectId: string, data: Partial<IPage>): Promise<IPage> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchPage(workspaceSlug: string, projectId: string, pageId: string, data: Partial<IPage>): Promise<IPage> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
console.error("error", error?.response?.data);
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deletePage(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addPageToFavorites(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-pages/`, { page: pageId })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removePageFromFavorites(workspaceSlug: string, projectId: string, pageId: string) {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-pages/${pageId}`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectPages(workspaceSlug: string, projectId: string): Promise<IPage[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getPagesWithParams(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageType: "all" | "favorite" | "private" | "shared"
|
||||
): Promise<IPage[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, {
|
||||
params: {
|
||||
page_view: pageType,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getPageDetails(workspaceSlug: string, projectId: string, pageId: string): Promise<IPage> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createPageBlock(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
data: Partial<IPageBlock>
|
||||
): Promise<IPageBlock> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getPageBlock(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
pageBlockId: string
|
||||
): Promise<IPageBlock[]> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchPageBlock(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
pageBlockId: string,
|
||||
data: Partial<IPageBlock>
|
||||
): Promise<IPage> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deletePageBlock(workspaceSlug: string, projectId: string, pageId: string, pageBlockId: string): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async listPageBlocks(workspaceSlug: string, projectId: string, pageId: string): Promise<IPageBlock[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async convertPageBlockToIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
blockId: string
|
||||
): Promise<TIssue> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${blockId}/issues/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
// =============== Archiving & Unarchiving Pages =================
|
||||
async archivePage(workspaceSlug: string, projectId: string, pageId: string): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/archive/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async restorePage(workspaceSlug: string, projectId: string, pageId: string): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/unarchive/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getArchivedPages(workspaceSlug: string, projectId: string): Promise<IPage[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-pages/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
// ==================== Pages Locking Services ==========================
|
||||
async lockPage(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/lock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async unlockPage(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/unlock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from "./project.service";
|
||||
export * from "./project-estimate.service";
|
||||
export * from "./project-export.service";
|
||||
export * from "./project-member.service";
|
||||
export * from "./project-state.service";
|
||||
export * from "./project-publish.service";
|
||||
@@ -1,72 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { IEstimate, IEstimateFormData, IEstimatePoint } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class ProjectEstimateService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async createEstimate(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IEstimateFormData
|
||||
): Promise<{
|
||||
estimate: IEstimate;
|
||||
estimate_points: IEstimatePoint[];
|
||||
}> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async patchEstimate(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
estimateId: string,
|
||||
data: IEstimateFormData
|
||||
): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getEstimateDetails(workspaceSlug: string, projectId: string, estimateId: string): Promise<IEstimate> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getEstimatesList(workspaceSlug: string, projectId: string): Promise<IEstimate[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteEstimate(workspaceSlug: string, projectId: string, estimateId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceEstimatesList(workspaceSlug: string): Promise<IEstimate[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/estimates/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class ProjectExportService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async csvExport(
|
||||
workspaceSlug: string,
|
||||
data: {
|
||||
provider: string;
|
||||
project: string[];
|
||||
}
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/export-issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { IProjectBulkAddFormData, IProjectMember, IProjectMembership } from "@plane/types";
|
||||
|
||||
export class ProjectMemberService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetchProjectMembers(workspaceSlug: string, projectId: string): Promise<IProjectMembership[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async bulkAddMembersToProject(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IProjectBulkAddFormData
|
||||
): Promise<IProjectMembership[]> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async projectMemberMe(workspaceSlug: string, projectId: string): Promise<IProjectMember> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise<IProjectMember> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateProjectMember(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
memberId: string,
|
||||
data: Partial<IProjectMember>
|
||||
): Promise<IProjectMember> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { IProjectPublishSettings } from "store/project/project-publish.store";
|
||||
|
||||
export class ProjectPublishService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getProjectSettingsAsync(workspace_slug: string, project_slug: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async createProjectSettingsAsync(
|
||||
workspace_slug: string,
|
||||
project_slug: string,
|
||||
data: IProjectPublishSettings
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async updateProjectSettingsAsync(
|
||||
workspace_slug: string,
|
||||
project_slug: string,
|
||||
project_publish_id: string,
|
||||
data: IProjectPublishSettings
|
||||
): Promise<any> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/${project_publish_id}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteProjectSettingsAsync(
|
||||
workspace_slug: string,
|
||||
project_slug: string,
|
||||
project_publish_id: string
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/${project_publish_id}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { IState } from "@plane/types";
|
||||
|
||||
export class ProjectStateService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async createState(workspaceSlug: string, projectId: string, data: any): Promise<IState> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async markDefault(workspaceSlug: string, projectId: string, stateId: string): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/mark-default/`, {})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async getStates(workspaceSlug: string, projectId: string): Promise<IState[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getState(workspaceSlug: string, projectId: string, stateId: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateState(workspaceSlug: string, projectId: string, stateId: string, data: IState): Promise<any> {
|
||||
return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async patchState(workspaceSlug: string, projectId: string, stateId: string, data: Partial<IState>): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteState(workspaceSlug: string, projectId: string, stateId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceStates(workspaceSlug: string): Promise<IState[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/states/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type {
|
||||
GithubRepositoriesResponse,
|
||||
IProject,
|
||||
ISearchIssueResponse,
|
||||
ProjectPreferences,
|
||||
IProjectViewProps,
|
||||
TProjectIssuesSearchParams,
|
||||
} from "@plane/types";
|
||||
|
||||
export class ProjectService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async createProject(workspaceSlug: string, data: Partial<IProject>): Promise<IProject> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async checkProjectIdentifierAvailability(workspaceSlug: string, data: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/project-identifiers`, {
|
||||
params: {
|
||||
name: data,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getProjects(workspaceSlug: string): Promise<IProject[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getProject(workspaceSlug: string, projectId: string): Promise<IProject> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateProject(workspaceSlug: string, projectId: string, data: Partial<IProject>): Promise<IProject> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteProject(workspaceSlug: string, projectId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async setProjectView(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: {
|
||||
view_props?: IProjectViewProps;
|
||||
default_props?: IProjectViewProps;
|
||||
preferences?: ProjectPreferences;
|
||||
sort_order?: number;
|
||||
}
|
||||
): Promise<any> {
|
||||
await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getGithubRepositories(url: string): Promise<GithubRepositoriesResponse> {
|
||||
return this.request({
|
||||
method: "get",
|
||||
url,
|
||||
headers: this.getAccessToken() ? this.getHeaders() : {},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async syncGithubRepository(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
workspaceIntegrationId: string,
|
||||
data: {
|
||||
name: string;
|
||||
owner: string;
|
||||
repository_id: string;
|
||||
url: string;
|
||||
}
|
||||
): Promise<any> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${workspaceIntegrationId}/github-repository-sync/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectGithubRepository(workspaceSlug: string, projectId: string, integrationId: string): Promise<any> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${integrationId}/github-repository-sync/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProjectFavorites(workspaceSlug: string): Promise<any[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-favorite-projects/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addProjectToFavorites(workspaceSlug: string, project: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/user-favorite-projects/`, { project })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeProjectFromFavorites(workspaceSlug: string, projectId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/user-favorite-projects/${projectId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async projectIssuesSearch(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
params: TProjectIssuesSearchParams
|
||||
): Promise<ISearchIssueResponse[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/search-issues/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import {
|
||||
IEmailCheckData,
|
||||
IEmailCheckResponse,
|
||||
ILoginTokenResponse,
|
||||
IMagicSignInData,
|
||||
IPasswordSignInData,
|
||||
} from "@plane/types";
|
||||
|
||||
export class AuthService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async emailCheck(data: IEmailCheckData): Promise<IEmailCheckResponse> {
|
||||
return this.post("/api/email-check/", data, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async passwordSignIn(data: IPasswordSignInData): Promise<ILoginTokenResponse> {
|
||||
return this.post("/api/sign-in/", data, { headers: {} })
|
||||
.then((response) => {
|
||||
this.setAccessToken(response?.data?.access_token);
|
||||
this.setRefreshToken(response?.data?.refresh_token);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async sendResetPasswordLink(data: { email: string }): Promise<any> {
|
||||
return this.post(`/api/forgot-password/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async setPassword(data: { password: string }): Promise<any> {
|
||||
return this.post(`/api/users/me/set-password/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async resetPassword(
|
||||
uidb64: string,
|
||||
token: string,
|
||||
data: {
|
||||
new_password: string;
|
||||
}
|
||||
): Promise<ILoginTokenResponse> {
|
||||
return this.post(`/api/reset-password/${uidb64}/${token}/`, data, { headers: {} })
|
||||
.then((response) => {
|
||||
if (response?.status === 200) {
|
||||
this.setAccessToken(response?.data?.access_token);
|
||||
this.setRefreshToken(response?.data?.refresh_token);
|
||||
return response?.data;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async emailSignUp(data: { email: string; password: string }): Promise<ILoginTokenResponse> {
|
||||
return this.post("/api/sign-up/", data, { headers: {} })
|
||||
.then((response) => {
|
||||
this.setAccessToken(response?.data?.access_token);
|
||||
this.setRefreshToken(response?.data?.refresh_token);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async socialAuth(data: any): Promise<ILoginTokenResponse> {
|
||||
return this.post("/api/social-auth/", data, { headers: {} })
|
||||
.then((response) => {
|
||||
this.setAccessToken(response?.data?.access_token);
|
||||
this.setRefreshToken(response?.data?.refresh_token);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async generateUniqueCode(data: { email: string }): Promise<any> {
|
||||
return this.post("/api/magic-generate/", data, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async magicSignIn(data: IMagicSignInData): Promise<any> {
|
||||
return await this.post("/api/magic-sign-in/", data, { headers: {} })
|
||||
.then((response) => {
|
||||
if (response?.status === 200) {
|
||||
this.setAccessToken(response?.data?.access_token);
|
||||
this.setRefreshToken(response?.data?.refresh_token);
|
||||
return response?.data;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async instanceAdminSignIn(data: IPasswordSignInData): Promise<ILoginTokenResponse> {
|
||||
return await this.post("/api/instances/admins/sign-in/", data, { headers: {} })
|
||||
.then((response) => {
|
||||
if (response?.status === 200) {
|
||||
this.setAccessToken(response?.data?.access_token);
|
||||
this.setRefreshToken(response?.data?.refresh_token);
|
||||
return response?.data;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async signOut(): Promise<any> {
|
||||
return this.post("/api/sign-out/", { refresh_token: this.getRefreshToken() })
|
||||
.then((response) => {
|
||||
this.purgeAccessToken();
|
||||
this.purgeRefreshToken();
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.purgeAccessToken();
|
||||
this.purgeRefreshToken();
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type {
|
||||
IUserNotification,
|
||||
INotificationParams,
|
||||
NotificationCount,
|
||||
PaginatedUserNotification,
|
||||
IMarkAllAsReadPayload,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class NotificationService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getUserNotifications(workspaceSlug: string, params: INotificationParams): Promise<IUserNotification[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/users/notifications`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserNotificationDetailById(workspaceSlug: string, notificationId: string): Promise<IUserNotification> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async markUserNotificationAsRead(workspaceSlug: string, notificationId: string): Promise<IUserNotification> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async markUserNotificationAsUnread(workspaceSlug: string, notificationId: string): Promise<IUserNotification> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async markUserNotificationAsArchived(workspaceSlug: string, notificationId: string): Promise<IUserNotification> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async markUserNotificationAsUnarchived(workspaceSlug: string, notificationId: string): Promise<IUserNotification> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchUserNotification(
|
||||
workspaceSlug: string,
|
||||
notificationId: string,
|
||||
data: Partial<IUserNotification>
|
||||
): Promise<IUserNotification> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserNotification(workspaceSlug: string, notificationId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async subscribeToIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getIssueNotificationSubscriptionStatus(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string
|
||||
): Promise<{
|
||||
subscribed: boolean;
|
||||
}> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async unsubscribeFromIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUnreadNotificationsCount(workspaceSlug: string): Promise<NotificationCount> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/unread/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getNotifications(url: string): Promise<PaginatedUserNotification> {
|
||||
return this.get(url)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async markAllNotificationsAsRead(workspaceSlug: string, payload: IMarkAllAsReadPayload): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/mark-all-read/`, {
|
||||
...payload,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type {
|
||||
TIssue,
|
||||
IUser,
|
||||
IUserActivityResponse,
|
||||
IInstanceAdminStatus,
|
||||
IUserProfileData,
|
||||
IUserProfileProjectSegregation,
|
||||
IUserSettings,
|
||||
IUserWorkspaceDashboard,
|
||||
IUserEmailNotificationSettings,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class UserService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
currentUserConfig() {
|
||||
return {
|
||||
url: `${this.baseURL}/api/users/me/`,
|
||||
headers: this.getHeaders(),
|
||||
};
|
||||
}
|
||||
|
||||
async userIssues(
|
||||
workspaceSlug: string,
|
||||
params: any
|
||||
): Promise<
|
||||
| {
|
||||
[key: string]: TIssue[];
|
||||
}
|
||||
| TIssue[]
|
||||
> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async currentUser(): Promise<IUser> {
|
||||
return this.get("/api/users/me/")
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async currentUserInstanceAdminStatus(): Promise<IInstanceAdminStatus> {
|
||||
return this.get("/api/users/me/instance-admin/")
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async currentUserSettings(): Promise<IUserSettings> {
|
||||
return this.get("/api/users/me/settings/")
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async currentUserEmailNotificationSettings(): Promise<IUserEmailNotificationSettings> {
|
||||
return this.get("/api/users/me/notification-preferences/")
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async updateUser(data: Partial<IUser>): Promise<any> {
|
||||
return this.patch("/api/users/me/", data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateUserOnBoard(): Promise<any> {
|
||||
return this.patch("/api/users/me/onboard/", {
|
||||
is_onboarded: true,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateUserTourCompleted(): Promise<any> {
|
||||
return this.patch("/api/users/me/tour-completed/", {
|
||||
is_tour_completed: true,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateCurrentUserEmailNotificationSettings(data: Partial<IUserEmailNotificationSettings>): Promise<any> {
|
||||
return this.patch("/api/users/me/notification-preferences/", data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserActivity(): Promise<IUserActivityResponse> {
|
||||
return this.get(`/api/users/me/activities/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async userWorkspaceDashboard(workspaceSlug: string, month: number): Promise<IUserWorkspaceDashboard> {
|
||||
return this.get(`/api/users/me/workspaces/${workspaceSlug}/dashboard/`, {
|
||||
params: {
|
||||
month: month,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async changePassword(data: { old_password: string; new_password: string; confirm_password: string }): Promise<any> {
|
||||
return this.post(`/api/users/me/change-password/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProfileData(workspaceSlug: string, userId: string): Promise<IUserProfileData> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-stats/${userId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProfileProjectsSegregation(
|
||||
workspaceSlug: string,
|
||||
userId: string
|
||||
): Promise<IUserProfileProjectSegregation> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-profile/${userId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProfileActivity(workspaceSlug: string, userId: string): Promise<IUserActivityResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/?per_page=15`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProfileIssues(workspaceSlug: string, userId: string, params: any): Promise<TIssue[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-issues/${userId}/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deactivateAccount() {
|
||||
return this.delete(`/api/users/me/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async leaveWorkspace(workspaceSlug: string) {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/members/leave/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async joinProject(workspaceSlug: string, project_ids: string[]): Promise<any> {
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/invitations/`, { project_ids })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async leaveProject(workspaceSlug: string, projectId: string) {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/leave/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { IProjectView } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class ViewService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async createView(workspaceSlug: string, projectId: string, data: Partial<IProjectView>): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchView(workspaceSlug: string, projectId: string, viewId: string, data: Partial<IProjectView>): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteView(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getViews(workspaceSlug: string, projectId: string): Promise<IProjectView[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getViewDetails(workspaceSlug: string, projectId: string, viewId: string): Promise<IProjectView> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getViewIssues(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/issues/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addViewToFavorites(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: {
|
||||
view: string;
|
||||
}
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeViewFromFavorites(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class AppInstallationService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async addInstallationApp(workspaceSlug: string, provider: string, data: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/workspace-integrations/${provider}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async addSlackChannel(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
integrationId: string | null | undefined,
|
||||
data: any
|
||||
): Promise<any> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${integrationId}/project-slack-sync/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async getSlackChannelDetail(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
integrationId: string | null | undefined
|
||||
): Promise<any> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${integrationId}/project-slack-sync/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async removeSlackChannel(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
integrationId: string | null | undefined,
|
||||
slackSyncId: string | undefined
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${integrationId}/project-slack-sync/${slackSyncId}`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import { THomeDashboardResponse, TWidget, TWidgetStatsResponse, TWidgetStatsRequestParams } from "@plane/types";
|
||||
|
||||
export class DashboardService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getHomeDashboardWidgets(workspaceSlug: string): Promise<THomeDashboardResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/dashboard/`, {
|
||||
params: {
|
||||
dashboard_type: "home",
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWidgetStats(
|
||||
workspaceSlug: string,
|
||||
dashboardId: string,
|
||||
params: TWidgetStatsRequestParams
|
||||
): Promise<TWidgetStatsResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/dashboard/${dashboardId}/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getDashboardDetails(dashboardId: string): Promise<TWidgetStatsResponse> {
|
||||
return this.get(`/api/dashboard/${dashboardId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateDashboardWidget(dashboardId: string, widgetId: string, data: Partial<TWidget>): Promise<TWidget> {
|
||||
return this.patch(`/api/dashboard/${dashboardId}/widgets/${widgetId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
// services
|
||||
import { APIService } from "../api.service";
|
||||
// types
|
||||
import {
|
||||
IWorkspace,
|
||||
IWorkspaceMemberMe,
|
||||
IWorkspaceMember,
|
||||
IWorkspaceMemberInvitation,
|
||||
ILastActiveWorkspaceDetails,
|
||||
IWorkspaceSearchResults,
|
||||
IProductUpdateResponse,
|
||||
IWorkspaceBulkInviteFormData,
|
||||
IWorkspaceViewProps,
|
||||
IUserProjectsRole,
|
||||
TIssue,
|
||||
IWorkspaceView,
|
||||
} from "@plane/types";
|
||||
|
||||
export interface IWorkspaceService {
|
||||
list(): Promise<IWorkspace[]>;
|
||||
retrieve(workspaceSlug: string): Promise<IWorkspace>;
|
||||
create(data: Partial<IWorkspace>): Promise<IWorkspace>;
|
||||
update(workspaceSlug: string, data: Partial<IWorkspace>): Promise<IWorkspace>;
|
||||
delete(workspaceSlug: string): Promise<undefined>;
|
||||
invite(
|
||||
workspaceSlug: string,
|
||||
data: IWorkspaceBulkInviteFormData
|
||||
): Promise<any>;
|
||||
join(workspaceSlug: string, invitationId: string, data: any): Promise<any>;
|
||||
joinMany(data: any): Promise<any>;
|
||||
getLastActiveWorkspaceAndProjects(): Promise<ILastActiveWorkspaceDetails>;
|
||||
userWorkspaceInvitations(): Promise<IWorkspaceMemberInvitation[]>;
|
||||
workspaceMemberMe(workspaceSlug: string): Promise<IWorkspaceMemberMe>;
|
||||
updateWorkspaceView(
|
||||
workspaceSlug: string,
|
||||
data: { view_props: IWorkspaceViewProps }
|
||||
): Promise<any>;
|
||||
fetchWorkspaceMembers(workspaceSlug: string): Promise<IWorkspaceMember[]>;
|
||||
updateWorkspaceMember(
|
||||
workspaceSlug: string,
|
||||
memberId: string,
|
||||
data: Partial<IWorkspaceMember>
|
||||
): Promise<IWorkspaceMember>;
|
||||
deleteWorkspaceMember(workspaceSlug: string, memberId: string): Promise<any>;
|
||||
workspaceInvitations(
|
||||
workspaceSlug: string
|
||||
): Promise<IWorkspaceMemberInvitation[]>;
|
||||
getWorkspaceInvitation(
|
||||
workspaceSlug: string,
|
||||
invitationId: string
|
||||
): Promise<IWorkspaceMemberInvitation>;
|
||||
updateWorkspaceInvitation(
|
||||
workspaceSlug: string,
|
||||
invitationId: string,
|
||||
data: Partial<IWorkspaceMember>
|
||||
): Promise<any>;
|
||||
deleteWorkspaceInvitations(
|
||||
workspaceSlug: string,
|
||||
invitationId: string
|
||||
): Promise<any>;
|
||||
workspaceSlugCheck(slug: string): Promise<any>;
|
||||
searchWorkspace(
|
||||
workspaceSlug: string,
|
||||
params: { project_id?: string; search: string; workspace_search: boolean }
|
||||
): Promise<IWorkspaceSearchResults>;
|
||||
getProductUpdates(): Promise<IProductUpdateResponse[]>;
|
||||
createView(
|
||||
workspaceSlug: string,
|
||||
data: Partial<IWorkspaceView>
|
||||
): Promise<IWorkspaceView>;
|
||||
updateView(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
data: Partial<IWorkspaceView>
|
||||
): Promise<IWorkspaceView>;
|
||||
deleteView(workspaceSlug: string, viewId: string): Promise<any>;
|
||||
getAllViews(workspaceSlug: string): Promise<IWorkspaceView[]>;
|
||||
getViewDetails(
|
||||
workspaceSlug: string,
|
||||
viewId: string
|
||||
): Promise<IWorkspaceView>;
|
||||
getViewIssues(workspaceSlug: string, params: any): Promise<TIssue[]>;
|
||||
getWorkspaceUserProjectsRole(
|
||||
workspaceSlug: string
|
||||
): Promise<IUserProjectsRole>;
|
||||
}
|
||||
|
||||
export class WorkspaceService extends APIService implements IWorkspaceService {
|
||||
constructor(BASE_URL: string) {
|
||||
super(BASE_URL);
|
||||
}
|
||||
|
||||
async list(): Promise<IWorkspace[]> {
|
||||
return this.get("/api/users/me/workspaces/")
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieve(workspaceSlug: string): Promise<IWorkspace> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: Partial<IWorkspace>): Promise<IWorkspace> {
|
||||
return this.post("/api/workspaces/", data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceSlug: string,
|
||||
data: Partial<IWorkspace>
|
||||
): Promise<IWorkspace> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async delete(workspaceSlug: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async invite(
|
||||
workspaceSlug: string,
|
||||
data: IWorkspaceBulkInviteFormData
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/invitations/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async join(
|
||||
workspaceSlug: string,
|
||||
invitationId: string,
|
||||
data: any
|
||||
): Promise<any> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/join/`,
|
||||
data,
|
||||
{
|
||||
headers: {},
|
||||
}
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async joinMany(data: any): Promise<any> {
|
||||
return this.post("/api/users/me/workspaces/invitations/", data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getLastActiveWorkspaceAndProjects(): Promise<ILastActiveWorkspaceDetails> {
|
||||
return this.get("/api/users/last-visited-workspace/")
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async userWorkspaceInvitations(): Promise<IWorkspaceMemberInvitation[]> {
|
||||
return this.get("/api/users/me/workspaces/invitations/")
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async workspaceMemberMe(workspaceSlug: string): Promise<IWorkspaceMemberMe> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/me/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async updateWorkspaceView(
|
||||
workspaceSlug: string,
|
||||
data: { view_props: IWorkspaceViewProps }
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchWorkspaceMembers(
|
||||
workspaceSlug: string
|
||||
): Promise<IWorkspaceMember[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/members/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateWorkspaceMember(
|
||||
workspaceSlug: string,
|
||||
memberId: string,
|
||||
data: Partial<IWorkspaceMember>
|
||||
): Promise<IWorkspaceMember> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/members/${memberId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteWorkspaceMember(
|
||||
workspaceSlug: string,
|
||||
memberId: string
|
||||
): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/members/${memberId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async workspaceInvitations(
|
||||
workspaceSlug: string
|
||||
): Promise<IWorkspaceMemberInvitation[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/invitations/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceInvitation(
|
||||
workspaceSlug: string,
|
||||
invitationId: string
|
||||
): Promise<IWorkspaceMemberInvitation> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/join/`,
|
||||
{ headers: {} }
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateWorkspaceInvitation(
|
||||
workspaceSlug: string,
|
||||
invitationId: string,
|
||||
data: Partial<IWorkspaceMember>
|
||||
): Promise<any> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteWorkspaceInvitations(
|
||||
workspaceSlug: string,
|
||||
invitationId: string
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async workspaceSlugCheck(slug: string): Promise<any> {
|
||||
return this.get(`/api/workspace-slug-check/?slug=${slug}`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async searchWorkspace(
|
||||
workspaceSlug: string,
|
||||
params: {
|
||||
project_id?: string;
|
||||
search: string;
|
||||
workspace_search: boolean;
|
||||
}
|
||||
): Promise<IWorkspaceSearchResults> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/search/`, {
|
||||
params,
|
||||
})
|
||||
.then((res) => res?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
async getProductUpdates(): Promise<IProductUpdateResponse[]> {
|
||||
return this.get("/api/release-notes/")
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createView(
|
||||
workspaceSlug: string,
|
||||
data: Partial<IWorkspaceView>
|
||||
): Promise<IWorkspaceView> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateView(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
data: Partial<IWorkspaceView>
|
||||
): Promise<IWorkspaceView> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteView(workspaceSlug: string, viewId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getAllViews(workspaceSlug: string): Promise<IWorkspaceView[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/views/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getViewDetails(
|
||||
workspaceSlug: string,
|
||||
viewId: string
|
||||
): Promise<IWorkspaceView> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getViewIssues(workspaceSlug: string, params: any): Promise<TIssue[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/issues/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceUserProjectsRole(
|
||||
workspaceSlug: string
|
||||
): Promise<IUserProjectsRole> {
|
||||
return this.get(`/api/users/me/workspaces/${workspaceSlug}/project-roles/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { IProjectLite, IWorkspaceLite } from "@plane/types";
|
||||
|
||||
export type GptApiResponse = {
|
||||
export interface IGptResponse {
|
||||
response: string;
|
||||
response_html: string;
|
||||
count: number;
|
||||
project_detail: IProjectLite;
|
||||
workspace_detail: IWorkspaceLite;
|
||||
};
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -30,7 +30,7 @@ export interface ICycle {
|
||||
is_favorite: boolean;
|
||||
issue: string;
|
||||
name: string;
|
||||
owned_by: string;
|
||||
owned_by: IUser;
|
||||
project: string;
|
||||
project_detail: IProjectLite;
|
||||
status: TCycleGroups;
|
||||
|
||||
Vendored
+1
@@ -14,6 +14,7 @@ export interface IInstance {
|
||||
namespace: string | null;
|
||||
is_telemetry_enabled: boolean;
|
||||
is_support_required: boolean;
|
||||
is_signup_screen_visited: boolean;
|
||||
created_by: string | null;
|
||||
updated_by: string | null;
|
||||
is_activated: boolean;
|
||||
|
||||
@@ -2,6 +2,8 @@ import * as React from "react";
|
||||
|
||||
// icons
|
||||
import { ChevronRight } from "lucide-react";
|
||||
// components
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
type BreadcrumbsProps = {
|
||||
children: any;
|
||||
@@ -23,11 +25,42 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
|
||||
type Props = {
|
||||
type?: "text" | "component";
|
||||
component?: React.ReactNode;
|
||||
link?: JSX.Element;
|
||||
label?: string;
|
||||
icon?: React.ReactNode;
|
||||
link?: string;
|
||||
};
|
||||
const BreadcrumbItem: React.FC<Props> = (props) => {
|
||||
const { type = "text", component, link } = props;
|
||||
return <>{type != "text" ? <div className="flex items-center space-x-2">{component}</div> : link}</>;
|
||||
const { type = "text", component, label, icon, link } = props;
|
||||
return (
|
||||
<>
|
||||
{type != "text" ? (
|
||||
<div className="flex items-center space-x-2">{component}</div>
|
||||
) : (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
<li className="flex items-center space-x-2">
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
{link ? (
|
||||
<a
|
||||
className="flex items-center gap-1 text-sm font-medium text-custom-text-300 hover:text-custom-text-100"
|
||||
href={link}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
|
||||
)}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</a>
|
||||
) : (
|
||||
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
|
||||
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Breadcrumbs.BreadcrumbItem = BreadcrumbItem;
|
||||
|
||||
@@ -47,7 +47,6 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
|
||||
>
|
||||
<Icon
|
||||
size={size}
|
||||
viewBox="0 0 23.5 24"
|
||||
className={cn(
|
||||
{
|
||||
"text-white": priority === "urgent",
|
||||
|
||||
@@ -89,8 +89,8 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="">
|
||||
<div className="flex items-start gap-x-4">
|
||||
<div className="grid place-items-center rounded-full bg-red-500/20 p-2 sm:p-2 md:p-4 lg:p-4 mt-3 sm:mt-3 md:mt-0 lg:mt-0 ">
|
||||
<Trash2 className="h-4 w-4 sm:h-4 sm:w-4 md:h-6 md:w-6 lg:h-6 lg:w-6 text-red-600" aria-hidden="true" />
|
||||
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<Trash2 className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<Dialog.Title as="h3" className="my-4 text-2xl font-medium leading-6 text-custom-text-100">
|
||||
|
||||
@@ -31,6 +31,7 @@ export const GoogleSignInButton: FC<Props> = (props) => {
|
||||
size: "large",
|
||||
logo_alignment: "center",
|
||||
text: type === "sign_in" ? "signin_with" : "signup_with",
|
||||
width: 384,
|
||||
} as GsiButtonConfiguration // customization attributes
|
||||
);
|
||||
} catch (err) {
|
||||
|
||||
@@ -8,8 +8,6 @@ import useToast from "hooks/use-toast";
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// icons
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
@@ -33,7 +31,6 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
const { email, handleSignInRedirection } = props;
|
||||
// states
|
||||
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// form info
|
||||
@@ -117,30 +114,17 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
required: "Password is required",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
minLength={8}
|
||||
autoFocus
|
||||
/>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
minLength={8}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Eye, EyeOff, XCircle } from "lucide-react";
|
||||
import { XCircle } from "lucide-react";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
@@ -40,7 +40,6 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
||||
const { email, handleStepChange, handleEmailClear, onSubmit } = props;
|
||||
// states
|
||||
const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
const {
|
||||
@@ -158,28 +157,15 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
||||
required: "Password is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
autoFocus
|
||||
/>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="w-full text-right mt-2 pb-3">
|
||||
|
||||
@@ -10,8 +10,6 @@ import { Button, Input } from "@plane/ui";
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// constants
|
||||
import { ESignUpSteps } from "components/account";
|
||||
// icons
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
@@ -36,7 +34,6 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
const { email, handleSignInRedirection } = props;
|
||||
// states
|
||||
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// form info
|
||||
@@ -122,29 +119,16 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
required: "Password is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
minLength={8}
|
||||
autoFocus
|
||||
/>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
minLength={8}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Eye, EyeOff, XCircle } from "lucide-react";
|
||||
import { XCircle } from "lucide-react";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
@@ -32,8 +32,6 @@ const authService = new AuthService();
|
||||
|
||||
export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
|
||||
const { onSubmit } = props;
|
||||
// states
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// form info
|
||||
@@ -114,28 +112,15 @@ export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
|
||||
required: "Password is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
autoFocus
|
||||
/>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useCycle, useMember, useModule, useProject } from "hooks/store";
|
||||
import { useCycle, useModule, useProject } from "hooks/store";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
@@ -15,12 +15,10 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
||||
const { getProjectById } = useProject();
|
||||
const { getCycleById } = useCycle();
|
||||
const { getModuleById } = useModule();
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -31,7 +29,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Lead</h6>
|
||||
<span>{cycleOwnerDetails?.display_name}</span>
|
||||
<span>{cycleDetails.owned_by?.display_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Start Date</h6>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import Link from "next/link";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
href?: string;
|
||||
icon?: React.ReactNode | undefined;
|
||||
};
|
||||
|
||||
export const BreadcrumbLink: React.FC<Props> = (props) => {
|
||||
const { href, label, icon } = props;
|
||||
return (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
<li className="flex items-center space-x-2">
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
{href ? (
|
||||
<Link
|
||||
className="flex items-center gap-1 text-sm font-medium text-custom-text-300 hover:text-custom-text-100"
|
||||
href={href}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
|
||||
)}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
|
||||
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./product-updates-modal";
|
||||
export * from "./empty-state";
|
||||
export * from "./latest-feature-block";
|
||||
export * from "./breadcrumb-link";
|
||||
|
||||
@@ -130,29 +130,17 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
||||
onChange(unsplashImages[0].urls.regular);
|
||||
}, [value, onChange, unsplashImages]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
};
|
||||
const openDropdown = () => setIsOpen(true);
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
useOutsideClickDetector(ref, closeDropdown);
|
||||
|
||||
return (
|
||||
<Popover className="relative z-[2]" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}>
|
||||
<Popover.Button
|
||||
className="rounded border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={handleOnClick}
|
||||
onClick={openDropdown}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -157,7 +157,6 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
||||
window.removeEventListener("keydown", handleEnterKeyPress);
|
||||
window.removeEventListener("keydown", handleEscapeKeyPress);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, handleSubmit, onClose]);
|
||||
|
||||
const responseActionButton = response !== "" && (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store";
|
||||
import { useCycle, useIssues, useProject, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { SingleProgressStats } from "components/core";
|
||||
@@ -58,7 +58,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
removeCycleFromFavorites,
|
||||
} = useCycle();
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getUserDetails } = useMember();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -68,7 +67,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
);
|
||||
|
||||
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
|
||||
const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by) : undefined;
|
||||
|
||||
const { data: activeCycleIssues } = useSWR(
|
||||
workspaceSlug && projectId && currentProjectActiveCycleId
|
||||
@@ -205,20 +203,20 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2.5 text-custom-text-200">
|
||||
{cycleOwnerDetails?.avatar && cycleOwnerDetails?.avatar !== "" ? (
|
||||
{activeCycle.owned_by.avatar && activeCycle.owned_by.avatar !== "" ? (
|
||||
<img
|
||||
src={cycleOwnerDetails?.avatar}
|
||||
src={activeCycle.owned_by.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-full"
|
||||
alt={cycleOwnerDetails?.display_name}
|
||||
alt={activeCycle.owned_by.display_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
|
||||
{cycleOwnerDetails?.display_name.charAt(0)}
|
||||
{activeCycle.owned_by.display_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-custom-text-200">{cycleOwnerDetails?.display_name}</span>
|
||||
<span className="text-custom-text-200">{activeCycle.owned_by.display_name}</span>
|
||||
</div>
|
||||
|
||||
{activeCycle.assignees.length > 0 && (
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Disclosure, Popover, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { CycleService } from "services/cycle.service";
|
||||
// hooks
|
||||
import { useApplication, useCycle, useMember, useUser } from "hooks/store";
|
||||
import { useApplication, useCycle, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { SidebarProgressStats } from "components/core";
|
||||
@@ -73,10 +73,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { getCycleById, updateCycleDetails } = useCycle();
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
const cycleDetails = getCycleById(cycleId);
|
||||
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -520,8 +518,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
<div className="flex w-1/2 items-center rounded-sm">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Avatar name={cycleOwnerDetails?.display_name} src={cycleOwnerDetails?.avatar} />
|
||||
<span className="text-sm text-custom-text-200">{cycleOwnerDetails?.display_name}</span>
|
||||
<Avatar name={cycleDetails.owned_by.display_name} src={cycleDetails.owned_by.avatar} />
|
||||
<span className="text-sm text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -589,16 +587,14 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{cycleDetails && cycleDetails.distribution && (
|
||||
<div className="relative h-40 w-80">
|
||||
<ProgressChart
|
||||
distribution={cycleDetails.distribution?.completion_chart ?? {}}
|
||||
startDate={cycleDetails.start_date ?? ""}
|
||||
endDate={cycleDetails.end_date ?? ""}
|
||||
totalIssues={cycleDetails.total_issues}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative h-40 w-80">
|
||||
<ProgressChart
|
||||
distribution={cycleDetails.distribution?.completion_chart ?? {}}
|
||||
startDate={cycleDetails.start_date ?? ""}
|
||||
endDate={cycleDetails.end_date ?? ""}
|
||||
totalIssues={cycleDetails.total_issues}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
|
||||
@@ -26,11 +26,11 @@ export const DurationFilterDropdown: React.FC<Props> = (props) => {
|
||||
placement="bottom-end"
|
||||
closeOnSelect
|
||||
>
|
||||
{DURATION_FILTER_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem key={option.key} onClick={() => onChange(option.key)}>
|
||||
{option.label}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
{DURATION_FILTER_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem key={option.key} onClick={() => onChange(option.key)}>
|
||||
{option.label}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
||||
})}
|
||||
>
|
||||
Issues
|
||||
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl px-3 flex items-center text-center justify-center">
|
||||
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium py-1 px-1.5 rounded-xl h-4 min-w-6 flex items-center text-center justify-center">
|
||||
{totalIssues}
|
||||
</span>
|
||||
</h6>
|
||||
|
||||
@@ -72,14 +72,14 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
||||
startedCount > 0
|
||||
? "started"
|
||||
: unStartedCount > 0
|
||||
? "unstarted"
|
||||
: backlogCount > 0
|
||||
? "backlog"
|
||||
: completedCount > 0
|
||||
? "completed"
|
||||
: canceledCount > 0
|
||||
? "cancelled"
|
||||
: null;
|
||||
? "unstarted"
|
||||
: backlogCount > 0
|
||||
? "backlog"
|
||||
: completedCount > 0
|
||||
? "completed"
|
||||
: canceledCount > 0
|
||||
? "cancelled"
|
||||
: null;
|
||||
|
||||
setActiveStateGroup(stateGroup);
|
||||
setDefaultStateGroup(stateGroup);
|
||||
@@ -151,13 +151,13 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
||||
/>
|
||||
</div>
|
||||
{totalCount > 0 ? (
|
||||
<div className="flex items-center pl-10 md:pl-11 lg:pl-14 pr-11 mt-11">
|
||||
<div className="flex flex-col sm:flex-row md:flex-row lg:flex-row items-center justify-evenly gap-x-10 gap-y-8 w-full">
|
||||
<div>
|
||||
<div className="flex items-center pl-20 md:pl-11 lg:pl-14 pr-11 mt-11">
|
||||
<div className="flex md:flex-col lg:flex-row items-center gap-x-10 gap-y-8 w-full">
|
||||
<div className="w-full flex justify-center">
|
||||
<PieGraph
|
||||
data={chartData}
|
||||
height="220px"
|
||||
width="200px"
|
||||
width="220px"
|
||||
innerRadius={0.6}
|
||||
cornerRadius={5}
|
||||
colors={(datum) => datum.data.color}
|
||||
@@ -189,7 +189,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
||||
layers={["arcs", CenteredMetric]}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-6 w-min whitespace-nowrap">
|
||||
<div className="justify-self-end space-y-6 w-min whitespace-nowrap">
|
||||
{chartData.map((item) => (
|
||||
<div key={item.id} className="flex items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-2.5 w-24">
|
||||
|
||||
@@ -7,9 +7,9 @@ import { useDashboard } from "hooks/store";
|
||||
import { WidgetLoader } from "components/dashboard/widgets";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { TOverviewStatsWidgetResponse } from "@plane/types";
|
||||
import { cn } from "helpers/common.helper";
|
||||
|
||||
export type WidgetProps = {
|
||||
dashboardId: string;
|
||||
@@ -72,18 +72,10 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
[&>div:nth-child(2)>a>div]:lg:border-r
|
||||
"
|
||||
>
|
||||
{STATS_LIST.map((stat, index) => (
|
||||
<div
|
||||
className={cn(
|
||||
`w-full flex flex-col gap-2 hover:bg-custom-background-80`,
|
||||
index === 0 ? "rounded-tl-xl lg:rounded-l-xl" : "",
|
||||
index === STATS_LIST.length - 1 ? "rounded-br-xl lg:rounded-r-xl" : "",
|
||||
index === 1 ? "rounded-tr-xl lg:rounded-[0px]" : "",
|
||||
index == 2 ? "rounded-bl-xl lg:rounded-[0px]" : ""
|
||||
)}
|
||||
>
|
||||
{STATS_LIST.map((stat) => (
|
||||
<div className="w-full flex flex-col gap-2 hover:bg-custom-background-80 rounded-[10px]">
|
||||
<Link href={stat.link} className="py-4 duration-300 rounded-[10px] w-full ">
|
||||
<div className={`relative flex pl-10 sm:pl-20 md:pl-20 lg:pl-20 items-center`}>
|
||||
<div className={`relative flex justify-center items-center`}>
|
||||
<div>
|
||||
<h5 className="font-semibold text-xl">{stat.count}</h5>
|
||||
<p className="text-custom-text-300 text-sm xl:text-base">{stat.title}</p>
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { TButtonVariants } from "./types";
|
||||
// constants
|
||||
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
|
||||
export type DropdownButtonProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
isActive: boolean;
|
||||
tooltipContent: string | React.ReactNode;
|
||||
tooltipHeading: string;
|
||||
showTooltip: boolean;
|
||||
variant: TButtonVariants;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
isActive: boolean;
|
||||
tooltipContent: string | React.ReactNode;
|
||||
tooltipHeading: string;
|
||||
showTooltip: boolean;
|
||||
};
|
||||
|
||||
export const DropdownButton: React.FC<DropdownButtonProps> = (props) => {
|
||||
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props;
|
||||
|
||||
const ButtonToRender: React.FC<ButtonProps> = BORDER_BUTTON_VARIANTS.includes(variant)
|
||||
? BorderButton
|
||||
: BACKGROUND_BUTTON_VARIANTS.includes(variant)
|
||||
? BackgroundButton
|
||||
: TransparentButton;
|
||||
|
||||
return (
|
||||
<ButtonToRender
|
||||
className={className}
|
||||
isActive={isActive}
|
||||
tooltipContent={tooltipContent}
|
||||
tooltipHeading={tooltipHeading}
|
||||
showTooltip={showTooltip}
|
||||
>
|
||||
{children}
|
||||
</ButtonToRender>
|
||||
);
|
||||
};
|
||||
|
||||
const BorderButton: React.FC<ButtonProps> = (props) => {
|
||||
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton: React.FC<ButtonProps> = (props) => {
|
||||
const { children, className, tooltipContent, tooltipHeading, showTooltip } = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton: React.FC<ButtonProps> = (props) => {
|
||||
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
// types
|
||||
import { TButtonVariants } from "./types";
|
||||
|
||||
export const BORDER_BUTTON_VARIANTS: TButtonVariants[] = ["border-with-text", "border-without-text"];
|
||||
|
||||
export const BACKGROUND_BUTTON_VARIANTS: TButtonVariants[] = ["background-with-text", "background-without-text"];
|
||||
|
||||
export const TRANSPARENT_BUTTON_VARIANTS: TButtonVariants[] = ["transparent-with-text", "transparent-without-text"];
|
||||
|
||||
export const BUTTON_VARIANTS_WITHOUT_TEXT: TButtonVariants[] = [
|
||||
"border-without-text",
|
||||
"background-without-text",
|
||||
"transparent-without-text",
|
||||
];
|
||||
|
||||
export const BUTTON_VARIANTS_WITH_TEXT: TButtonVariants[] = [
|
||||
"border-with-text",
|
||||
"background-with-text",
|
||||
"transparent-with-text",
|
||||
];
|
||||
@@ -7,16 +7,13 @@ import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { useApplication, useCycle } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
// icons
|
||||
import { ContrastIcon } from "@plane/ui";
|
||||
import { ContrastIcon, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { ICycle } from "@plane/types";
|
||||
import { TDropdownProps } from "./types";
|
||||
// constants
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
button?: ReactNode;
|
||||
@@ -27,6 +24,18 @@ type Props = TDropdownProps & {
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
className?: string;
|
||||
cycle: ICycle | null;
|
||||
hideIcon: boolean;
|
||||
hideText?: boolean;
|
||||
dropdownArrow: boolean;
|
||||
isActive?: boolean;
|
||||
dropdownArrowClassName: string;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
};
|
||||
|
||||
type DropdownOptions =
|
||||
| {
|
||||
value: string | null;
|
||||
@@ -35,6 +44,100 @@ type DropdownOptions =
|
||||
}[]
|
||||
| undefined;
|
||||
|
||||
const BorderButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
cycle,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}{" "}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
cycle,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
cycle,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
button,
|
||||
@@ -50,8 +153,8 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
placeholder = "Cycle",
|
||||
placement,
|
||||
projectId,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
@@ -118,34 +221,13 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedCycle = value ? getCycleById(value) : null;
|
||||
|
||||
const onOpen = () => {
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
const dropdownOnChange = (val: string | null) => {
|
||||
onChange(val);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@@ -154,7 +236,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full", className)}
|
||||
value={value}
|
||||
onChange={dropdownOnChange}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
@@ -164,7 +246,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={handleOnClick}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@@ -180,24 +262,77 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading="Cycle"
|
||||
tooltipContent={selectedCycle?.name ?? placeholder}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{selectedCycle?.name ?? placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
{/* TODO: move button components to a single file for each dropdown */}
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@@ -231,6 +366,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={closeDropdown}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
||||
@@ -2,19 +2,17 @@ import React, { useRef, useState } from "react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import { usePopper } from "react-popper";
|
||||
import { CalendarDays, X } from "lucide-react";
|
||||
import { Calendar, CalendarDays, X } from "lucide-react";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { TDropdownProps } from "./types";
|
||||
// constants
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
clearIconClassName?: string;
|
||||
@@ -25,6 +23,157 @@ type Props = TDropdownProps & {
|
||||
onChange: (val: Date | null) => void;
|
||||
value: Date | string | null;
|
||||
closeOnSelect?: boolean;
|
||||
showPlaceholderIcon?: boolean;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
className?: string;
|
||||
clearIconClassName: string;
|
||||
date: string | Date | null;
|
||||
icon: React.ReactNode;
|
||||
isClearable: boolean;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
isActive?: boolean;
|
||||
onClear: () => void;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
showPlaceholderIcon?: boolean;
|
||||
};
|
||||
|
||||
const BorderButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
clearIconClassName,
|
||||
date,
|
||||
icon,
|
||||
isClearable,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={date ? renderFormattedDate(date) : "None"}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
clearIconClassName,
|
||||
date,
|
||||
icon,
|
||||
isClearable,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={date ? renderFormattedDate(date) : "None"}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
clearIconClassName,
|
||||
date,
|
||||
icon,
|
||||
isClearable,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
showPlaceholderIcon = false,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={date ? renderFormattedDate(date) : "None"}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{showPlaceholderIcon && !date && (
|
||||
<Calendar className="h-2.5 w-2.5 flex-shrink-0 hidden group-hover:inline text-custom-text-400" />
|
||||
)}
|
||||
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const DateDropdown: React.FC<Props> = (props) => {
|
||||
@@ -44,8 +193,9 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
onChange,
|
||||
placeholder = "Date",
|
||||
placement,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
showPlaceholderIcon = false,
|
||||
value,
|
||||
} = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -67,36 +217,15 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
],
|
||||
});
|
||||
|
||||
const isDateSelected = value && value.toString().trim() !== "";
|
||||
const isDateSelected = value !== null && value !== undefined && value.toString().trim() !== "";
|
||||
|
||||
const onOpen = () => {
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
const dropdownOnChange = (val: Date | null) => {
|
||||
onChange(val);
|
||||
if (closeOnSelect) handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@@ -118,30 +247,90 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={value ? renderFormattedDate(value) : "None"}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{value ? renderFormattedDate(value) : placeholder}</span>
|
||||
)}
|
||||
{isClearable && isDateSelected && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onChange(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DropdownButton>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
showPlaceholderIcon={showPlaceholderIcon}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
showPlaceholderIcon={showPlaceholderIcon}
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
{isOpen && (
|
||||
@@ -149,7 +338,10 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<DatePicker
|
||||
selected={value ? new Date(value) : null}
|
||||
onChange={dropdownOnChange}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
if (closeOnSelect) closeDropdown();
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
|
||||
@@ -8,14 +8,12 @@ import sortBy from "lodash/sortBy";
|
||||
import { useApplication, useEstimate } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { TDropdownProps } from "./types";
|
||||
// constants
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
button?: ReactNode;
|
||||
@@ -26,6 +24,18 @@ type Props = TDropdownProps & {
|
||||
value: number | null;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
className?: string;
|
||||
estimatePoint: string | null;
|
||||
dropdownArrow: boolean;
|
||||
dropdownArrowClassName: string;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
isActive?: boolean;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
};
|
||||
|
||||
type DropdownOptions =
|
||||
| {
|
||||
value: number | null;
|
||||
@@ -34,6 +44,118 @@ type DropdownOptions =
|
||||
}[]
|
||||
| undefined;
|
||||
|
||||
const BorderButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
estimatePoint,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
estimatePoint,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
estimatePoint,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
button,
|
||||
@@ -49,8 +171,8 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
placeholder = "Estimate",
|
||||
placement,
|
||||
projectId,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
@@ -106,35 +228,15 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
|
||||
|
||||
const onOpen = () => {
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
|
||||
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
const dropdownOnChange = (val: number | null) => {
|
||||
onChange(val);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@@ -143,7 +245,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full w-full", className)}
|
||||
value={value}
|
||||
onChange={dropdownOnChange}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
@@ -153,7 +255,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={handleOnClick}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@@ -169,24 +271,76 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={selectedEstimate !== null ? selectedEstimate : placeholder}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{selectedEstimate !== null ? selectedEstimate : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@@ -220,6 +374,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={closeDropdown}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from "./cycle";
|
||||
export * from "./date";
|
||||
export * from "./estimate";
|
||||
export * from "./module";
|
||||
export * from "./module-select";
|
||||
export * from "./priority";
|
||||
export * from "./project";
|
||||
export * from "./state";
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMember } from "hooks/store";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui";
|
||||
|
||||
type AvatarProps = {
|
||||
showTooltip: boolean;
|
||||
userIds: string | string[] | null;
|
||||
};
|
||||
|
||||
export const ButtonAvatars: React.FC<AvatarProps> = observer((props) => {
|
||||
const { showTooltip, userIds } = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
if (Array.isArray(userIds)) {
|
||||
if (userIds.length > 0)
|
||||
return (
|
||||
<AvatarGroup size="md" showTooltip={!showTooltip}>
|
||||
{userIds.map((userId) => {
|
||||
const userDetails = getUserDetails(userId);
|
||||
|
||||
if (!userDetails) return;
|
||||
return <Avatar key={userId} src={userDetails.avatar} name={userDetails.display_name} />;
|
||||
})}
|
||||
</AvatarGroup>
|
||||
);
|
||||
} else {
|
||||
if (userIds) {
|
||||
const userDetails = getUserDetails(userIds);
|
||||
return <Avatar src={userDetails?.avatar} name={userDetails?.display_name} size="md" showTooltip={!showTooltip} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <UserGroupIcon className="h-3 w-3 flex-shrink-0" />;
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// hooks
|
||||
import { useMember } from "hooks/store";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, Tooltip, UserGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
|
||||
type ButtonProps = {
|
||||
className?: string;
|
||||
dropdownArrow: boolean;
|
||||
dropdownArrowClassName: string;
|
||||
placeholder: string;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip: boolean;
|
||||
userIds: string | string[] | null;
|
||||
};
|
||||
|
||||
const ButtonAvatars = observer(({ tooltip, userIds }: { tooltip: boolean; userIds: string | string[] | null }) => {
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
if (Array.isArray(userIds)) {
|
||||
if (userIds.length > 0)
|
||||
return (
|
||||
<AvatarGroup size="md" showTooltip={!tooltip}>
|
||||
{userIds.map((userId) => {
|
||||
const userDetails = getUserDetails(userId);
|
||||
|
||||
if (!userDetails) return;
|
||||
return <Avatar key={userId} src={userDetails.avatar} name={userDetails.display_name} />;
|
||||
})}
|
||||
</AvatarGroup>
|
||||
);
|
||||
} else {
|
||||
if (userIds) {
|
||||
const userDetails = getUserDetails(userIds);
|
||||
return <Avatar src={userDetails?.avatar} name={userDetails?.display_name} size="md" showTooltip={!tooltip} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <UserGroupIcon className="h-3 w-3 flex-shrink-0" />;
|
||||
});
|
||||
|
||||
export const BorderButton = observer((props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
userIds,
|
||||
tooltip,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
const isArray = Array.isArray(userIds);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{isArray && userIds.length > 0
|
||||
? userIds.length === 1
|
||||
? getUserDetails(userIds[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
export const BackgroundButton = observer((props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
userIds,
|
||||
tooltip,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
const isArray = Array.isArray(userIds);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{isArray && userIds.length > 0
|
||||
? userIds.length === 1
|
||||
? getUserDetails(userIds[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
export const TransparentButton = observer((props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
userIds,
|
||||
tooltip,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
const isArray = Array.isArray(userIds);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{isArray && userIds.length > 0
|
||||
? userIds.length === 1
|
||||
? getUserDetails(userIds[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./buttons";
|
||||
export * from "./project-member";
|
||||
export * from "./workspace-member";
|
||||
|
||||
@@ -2,22 +2,19 @@ import { Fragment, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { Check, Search } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useMember, useUser } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { ButtonAvatars } from "./avatar";
|
||||
import { DropdownButton } from "../buttons";
|
||||
import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns";
|
||||
// icons
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { MemberDropdownProps } from "./types";
|
||||
// constants
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
@@ -39,8 +36,8 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
placeholder = "Members",
|
||||
placement,
|
||||
projectId,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
@@ -99,35 +96,15 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
|
||||
const onOpen = () => {
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
|
||||
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
const dropdownOnChange = (val: string & string[]) => {
|
||||
onChange(val);
|
||||
if (!multiple) handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@@ -135,7 +112,6 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full", className)}
|
||||
onChange={dropdownOnChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...comboboxProps}
|
||||
>
|
||||
@@ -145,7 +121,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={handleOnClick}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@@ -161,30 +137,76 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{Array.isArray(value) && value.length > 0
|
||||
? value.length === 1
|
||||
? getUserDetails(value[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@@ -218,6 +240,9 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={() => {
|
||||
if (!multiple) closeDropdown();
|
||||
}}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
||||
@@ -2,22 +2,19 @@ import { Fragment, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { Check, Search } from "lucide-react";
|
||||
// hooks
|
||||
import { useMember, useUser } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { ButtonAvatars } from "./avatar";
|
||||
import { DropdownButton } from "../buttons";
|
||||
import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns";
|
||||
// icons
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { MemberDropdownProps } from "./types";
|
||||
// constants
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
|
||||
|
||||
export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((props) => {
|
||||
const {
|
||||
@@ -34,13 +31,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
onChange,
|
||||
placeholder = "Members",
|
||||
placement,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
@@ -90,34 +87,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
};
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
|
||||
const onOpen = () => {
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
const dropdownOnChange = (val: string & string[]) => {
|
||||
onChange(val);
|
||||
if (!multiple) handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@@ -127,7 +103,6 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
className={cn("h-full", className)}
|
||||
{...comboboxProps}
|
||||
handleKeyDown={handleKeyDown}
|
||||
onChange={dropdownOnChange}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
@@ -135,7 +110,6 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@@ -151,82 +125,124 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{Array.isArray(value) && value.length > 0
|
||||
? value.length === 1
|
||||
? getUserDetails(value[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
{isOpen && (
|
||||
<Combobox.Options className="fixed z-10" static>
|
||||
<div
|
||||
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span className="flex-grow truncate">{option.content}</span>
|
||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<div
|
||||
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={() => {
|
||||
if (!multiple) closeDropdown();
|
||||
}}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span className="flex-grow truncate">{option.content}</span>
|
||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user