Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1604f4585d | |||
| 03f9ca45d8 | |||
| 58ea4d6ec9 |
@@ -11,7 +11,9 @@ from .workspace import (
|
||||
)
|
||||
from .project import (
|
||||
ProjectSerializer,
|
||||
ProjectSettingSerializer,
|
||||
ProjectDetailSerializer,
|
||||
ProjectSettingDetailSerializer,
|
||||
ProjectMemberSerializer,
|
||||
ProjectMemberInviteSerializer,
|
||||
ProjectIdentifierSerializer,
|
||||
@@ -20,7 +22,7 @@ from .project import (
|
||||
ProjectMemberLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer
|
||||
ProjectPublicMemberSerializer,
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
|
||||
@@ -10,6 +10,7 @@ from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSe
|
||||
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectSetting,
|
||||
ProjectMember,
|
||||
ProjectMemberInvite,
|
||||
ProjectIdentifier,
|
||||
@@ -96,8 +97,6 @@ class ProjectLiteSerializer(BaseSerializer):
|
||||
|
||||
class ProjectDetailSerializer(BaseSerializer):
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
default_assignee = UserLiteSerializer(read_only=True)
|
||||
project_lead = UserLiteSerializer(read_only=True)
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_cycles = serializers.IntegerField(read_only=True)
|
||||
@@ -178,12 +177,12 @@ class ProjectDeployBoardSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project", "anchor",
|
||||
"project",
|
||||
"anchor",
|
||||
]
|
||||
|
||||
|
||||
class ProjectPublicMemberSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProjectPublicMember
|
||||
fields = "__all__"
|
||||
@@ -192,3 +191,26 @@ class ProjectPublicMemberSerializer(BaseSerializer):
|
||||
"project",
|
||||
"member",
|
||||
]
|
||||
|
||||
|
||||
class ProjectSettingSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = ProjectSetting
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
|
||||
class ProjectSettingDetailSerializer(BaseSerializer):
|
||||
default_assignee = UserLiteSerializer(read_only=True)
|
||||
project_lead = UserLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectSetting
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
@@ -59,6 +59,7 @@ from plane.api.views import (
|
||||
## End File Assets
|
||||
# Projects
|
||||
ProjectViewSet,
|
||||
ProjectSettingViewSet,
|
||||
InviteProjectEndpoint,
|
||||
ProjectMemberViewSet,
|
||||
ProjectMemberEndpoint,
|
||||
@@ -481,6 +482,26 @@ urlpatterns = [
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/settings/",
|
||||
ProjectSettingViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/settings/",
|
||||
ProjectSettingViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
}
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-identifiers/",
|
||||
ProjectIdentifierEndpoint.as_view(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .project import (
|
||||
ProjectViewSet,
|
||||
ProjectSettingViewSet,
|
||||
ProjectMemberViewSet,
|
||||
UserProjectInvitationsViewset,
|
||||
InviteProjectEndpoint,
|
||||
|
||||
@@ -9,7 +9,7 @@ from sentry_sdk import capture_exception
|
||||
# Module imports
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Project, Estimate, EstimatePoint
|
||||
from plane.db.models import ProjectSetting, Estimate, EstimatePoint
|
||||
from plane.api.serializers import (
|
||||
EstimateSerializer,
|
||||
EstimatePointSerializer,
|
||||
@@ -24,10 +24,10 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
try:
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
if project.estimate_id is not None:
|
||||
project_setting = ProjectSetting.objects.get(workspace__slug=slug, pk=project_id)
|
||||
if project_setting.estimate_id is not None:
|
||||
estimate_points = EstimatePoint.objects.filter(
|
||||
estimate_id=project.estimate_id,
|
||||
estimate_id=project_setting.estimate_id,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
@@ -63,6 +63,7 @@ from plane.api.permissions import (
|
||||
)
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectSetting,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
@@ -111,6 +112,26 @@ class IssueViewSet(BaseViewSet):
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def perform_update(self, serializer):
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = (
|
||||
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||
)
|
||||
if current_instance is not None:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch=int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
current_instance = (
|
||||
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||
@@ -273,13 +294,14 @@ class IssueViewSet(BaseViewSet):
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
project = Project.objects.get(pk=project_id)
|
||||
project_setting = ProjectSetting.objects.get(workspace__slug=slug, project_id=project_id)
|
||||
|
||||
serializer = IssueCreateSerializer(
|
||||
data=request.data,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"workspace_id": project.workspace_id,
|
||||
"default_assignee_id": project.default_assignee_id,
|
||||
"default_assignee_id": project_setting.default_assignee_id,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -320,35 +342,6 @@ class IssueViewSet(BaseViewSet):
|
||||
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
# Track this data
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch=int(timezone.now().timestamp())
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Issue.DoesNotExist:
|
||||
return Response({"error": "Isssue does not exists"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class UserWorkSpaceIssues(BaseAPIView):
|
||||
@method_decorator(gzip_page)
|
||||
@@ -2574,13 +2567,13 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
project_setting = ProjectSetting.objects.get(workspace__slug=slug, project_id=project_id)
|
||||
serializer = IssueCreateSerializer(
|
||||
data=request.data,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"workspace_id": project.workspace_id,
|
||||
"default_assignee_id": project.default_assignee_id,
|
||||
"default_assignee_id": project_setting.default_assignee_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -29,11 +29,12 @@ from sentry_sdk import capture_exception
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.api.serializers import (
|
||||
ProjectSerializer,
|
||||
ProjectSettingDetailSerializer,
|
||||
ProjectSettingSerializer,
|
||||
ProjectMemberSerializer,
|
||||
ProjectDetailSerializer,
|
||||
ProjectMemberInviteSerializer,
|
||||
ProjectFavoriteSerializer,
|
||||
IssueLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
)
|
||||
@@ -67,6 +68,7 @@ from plane.db.models import (
|
||||
ModuleMember,
|
||||
Inbox,
|
||||
ProjectDeployBoard,
|
||||
ProjectSetting,
|
||||
)
|
||||
|
||||
from plane.bgtasks.project_invitation_task import project_invitation
|
||||
@@ -98,7 +100,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
|
||||
.select_related(
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
"workspace", "workspace__owner",
|
||||
)
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.annotate(
|
||||
@@ -210,20 +212,29 @@ class ProjectViewSet(BaseViewSet):
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
project_lead = request.data.get("project_lead", None)
|
||||
|
||||
# Create Project Setting
|
||||
_ = ProjectSetting.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
project_lead_id=request.data.get("project_lead", None)
|
||||
)
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
project_member = ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"], member=request.user, role=20
|
||||
)
|
||||
|
||||
if serializer.data["project_lead"] is not None and str(
|
||||
serializer.data["project_lead"]
|
||||
if project_lead is not None and str(
|
||||
project_lead
|
||||
) != str(request.user.id):
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member_id=serializer.data["project_lead"],
|
||||
member_id=project_lead,
|
||||
role=20,
|
||||
)
|
||||
|
||||
|
||||
# Default states
|
||||
states = [
|
||||
{
|
||||
@@ -307,7 +318,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -973,7 +984,7 @@ class ProjectFavoritesViewSet(BaseViewSet):
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(user=self.request.user)
|
||||
.select_related(
|
||||
"project", "project__project_lead", "project__default_assignee"
|
||||
"project",
|
||||
)
|
||||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
@@ -1246,3 +1257,69 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response([], status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProjectSettingViewSet(BaseViewSet):
|
||||
model = ProjectSetting
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_serializer_class(self, *args, **kwargs):
|
||||
if self.action in ["create", "partial_update"]:
|
||||
return ProjectSettingSerializer
|
||||
return ProjectSettingDetailSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
try:
|
||||
project_setting = self.get_queryset().first()
|
||||
|
||||
if project_setting is not None:
|
||||
serializer = ProjectSettingDetailSerializer(project_setting)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
{"error": "Project setting does not exists"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id):
|
||||
try:
|
||||
project_setting = self.get_queryset().first()
|
||||
|
||||
# Check if it is None
|
||||
if project_setting is not None:
|
||||
serializer = ProjectSettingSerializer(
|
||||
project_setting, data=request.data, partial=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"error": "Project setting does not exists"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue, Project, State
|
||||
from plane.db.models import Issue, ProjectSetting, State
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ def archive_and_close_old_issues():
|
||||
def archive_old_issues():
|
||||
try:
|
||||
# Get all the projects whose archive_in is greater than 0
|
||||
projects = Project.objects.filter(archive_in__gt=0)
|
||||
project_settings = ProjectSetting.objects.filter(archive_in__gt=0)
|
||||
|
||||
for project in projects:
|
||||
project_id = project.id
|
||||
archive_in = project.archive_in
|
||||
for project_setting in project_settings:
|
||||
project_id = project_setting.project_id
|
||||
archive_in = project_setting.archive_in
|
||||
|
||||
# Get all the issues whose updated_at in less that the archive_in month
|
||||
issues = Issue.issue_objects.filter(
|
||||
@@ -75,7 +75,7 @@ def archive_old_issues():
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_at": str(archive_at)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
actor_id=str(project_setting.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
@@ -95,13 +95,13 @@ def archive_old_issues():
|
||||
def close_old_issues():
|
||||
try:
|
||||
# Get all the projects whose close_in is greater than 0
|
||||
projects = Project.objects.filter(close_in__gt=0).select_related(
|
||||
"default_state"
|
||||
project_settings = ProjectSetting.objects.filter(close_in__gt=0).select_related(
|
||||
"close_state"
|
||||
)
|
||||
|
||||
for project in projects:
|
||||
project_id = project.id
|
||||
close_in = project.close_in
|
||||
for project_setting in project_settings:
|
||||
project_id = project_setting.project_id
|
||||
close_in = project_setting.close_in
|
||||
|
||||
# Get all the issues whose updated_at in less that the close_in month
|
||||
issues = Issue.issue_objects.filter(
|
||||
@@ -130,10 +130,10 @@ def close_old_issues():
|
||||
|
||||
# Check if Issues
|
||||
if issues:
|
||||
if project.default_state is None:
|
||||
if project_setting.close_state is None:
|
||||
close_state = State.objects.filter(group="cancelled").first()
|
||||
else:
|
||||
close_state = project.default_state
|
||||
close_state = project_setting.close_state
|
||||
|
||||
issues_to_update = []
|
||||
for issue in issues:
|
||||
@@ -147,7 +147,7 @@ def close_old_issues():
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
actor_id=str(project_setting.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# Generated by Django 4.2.3 on 2023-10-11 13:34
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def create_project_settings(apps, schema_editor):
|
||||
Project = apps.get_model("db", "Project")
|
||||
ProjectSetting = apps.get_model("db", "ProjectSetting")
|
||||
|
||||
ProjectSetting.objects.bulk_create(
|
||||
[
|
||||
ProjectSetting(
|
||||
project=project,
|
||||
workspace_id=project.workspace_id,
|
||||
archive_in=project.archive_in,
|
||||
close_in=project.close_in,
|
||||
cycle_view=project.cycle_view,
|
||||
default_assignee=project.default_assignee,
|
||||
default_state=project.default_state,
|
||||
estimate=project.estimate,
|
||||
inbox_view=project.inbox_view,
|
||||
issue_views_view=project.issue_views_view,
|
||||
module_view=project.module_view,
|
||||
page_view=project.page_view,
|
||||
project_lead=project.project_lead,
|
||||
)
|
||||
for project in Project.objects.all()
|
||||
],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProjectSetting',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('module_view', models.BooleanField(default=True)),
|
||||
('cycle_view', models.BooleanField(default=True)),
|
||||
('issue_views_view', models.BooleanField(default=True)),
|
||||
('page_view', models.BooleanField(default=True)),
|
||||
('inbox_view', models.BooleanField(default=False)),
|
||||
('archive_in', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(12)])),
|
||||
('close_in', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(12)])),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('default_assignee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='default_assignee', to=settings.AUTH_USER_MODEL)),
|
||||
('default_state', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_state', to='db.state')),
|
||||
('estimate', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='db.estimate')),
|
||||
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='project_settings', to='db.project')),
|
||||
('project_lead', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_lead', to=settings.AUTH_USER_MODEL)),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_settings', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Project Settings',
|
||||
'verbose_name_plural': 'Project Settings',
|
||||
'db_table': 'project_settings',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(create_project_settings),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='archive_in',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='close_in',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='cycle_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='default_assignee',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='default_state',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='description_html',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='description_text',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='estimate',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='inbox_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='issue_views_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='module_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='page_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='project_lead',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.3 on 2023-10-12 07:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0046_auto_20231011_1334'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='projectsetting',
|
||||
name='default_state',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projectsetting',
|
||||
name='close_state',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='close_state', to='db.state'),
|
||||
),
|
||||
]
|
||||
@@ -13,6 +13,7 @@ from .workspace import (
|
||||
|
||||
from .project import (
|
||||
Project,
|
||||
ProjectSetting,
|
||||
ProjectMember,
|
||||
ProjectBaseModel,
|
||||
ProjectMemberInvite,
|
||||
|
||||
@@ -38,7 +38,7 @@ def get_default_props():
|
||||
},
|
||||
"display_filters": {
|
||||
"group_by": None,
|
||||
"order_by": '-created_at',
|
||||
"order_by": "-created_at",
|
||||
"type": None,
|
||||
"sub_issue": True,
|
||||
"show_empty_groups": True,
|
||||
@@ -56,12 +56,6 @@ class Project(BaseModel):
|
||||
NETWORK_CHOICES = ((0, "Secret"), (2, "Public"))
|
||||
name = models.CharField(max_length=255, verbose_name="Project Name")
|
||||
description = models.TextField(verbose_name="Project Description", blank=True)
|
||||
description_text = models.JSONField(
|
||||
verbose_name="Project Description RT", blank=True, null=True
|
||||
)
|
||||
description_html = models.JSONField(
|
||||
verbose_name="Project Description HTML", blank=True, null=True
|
||||
)
|
||||
network = models.PositiveSmallIntegerField(default=2, choices=NETWORK_CHOICES)
|
||||
workspace = models.ForeignKey(
|
||||
"db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_project"
|
||||
@@ -70,40 +64,40 @@ class Project(BaseModel):
|
||||
max_length=12,
|
||||
verbose_name="Project Identifier",
|
||||
)
|
||||
default_assignee = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="default_assignee",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
project_lead = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="project_lead",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
# default_assignee = models.ForeignKey(
|
||||
# settings.AUTH_USER_MODEL,
|
||||
# on_delete=models.CASCADE,
|
||||
# related_name="default_assignee",
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# )
|
||||
# project_lead = models.ForeignKey(
|
||||
# settings.AUTH_USER_MODEL,
|
||||
# on_delete=models.CASCADE,
|
||||
# related_name="project_lead",
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# )
|
||||
emoji = models.CharField(max_length=255, null=True, blank=True)
|
||||
icon_prop = models.JSONField(null=True)
|
||||
module_view = models.BooleanField(default=True)
|
||||
cycle_view = models.BooleanField(default=True)
|
||||
issue_views_view = models.BooleanField(default=True)
|
||||
page_view = models.BooleanField(default=True)
|
||||
inbox_view = models.BooleanField(default=False)
|
||||
# module_view = models.BooleanField(default=True)
|
||||
# cycle_view = models.BooleanField(default=True)
|
||||
# issue_views_view = models.BooleanField(default=True)
|
||||
# page_view = models.BooleanField(default=True)
|
||||
# inbox_view = models.BooleanField(default=False)
|
||||
cover_image = models.URLField(blank=True, null=True, max_length=800)
|
||||
estimate = models.ForeignKey(
|
||||
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
|
||||
)
|
||||
archive_in = models.IntegerField(
|
||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
)
|
||||
close_in = models.IntegerField(
|
||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
)
|
||||
default_state = models.ForeignKey(
|
||||
"db.State", on_delete=models.SET_NULL, null=True, related_name="default_state"
|
||||
)
|
||||
# estimate = models.ForeignKey(
|
||||
# "db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
|
||||
# )
|
||||
# archive_in = models.IntegerField(
|
||||
# default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
# )
|
||||
# close_in = models.IntegerField(
|
||||
# default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
# )
|
||||
# default_state = models.ForeignKey(
|
||||
# "db.State", on_delete=models.SET_NULL, null=True, related_name="default_state"
|
||||
# )
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the project"""
|
||||
@@ -121,6 +115,60 @@ class Project(BaseModel):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ProjectSetting(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="project_settings"
|
||||
)
|
||||
project = models.OneToOneField(
|
||||
"db.Project", on_delete=models.CASCADE, related_name="project_settings"
|
||||
)
|
||||
default_assignee = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="default_assignee",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
project_lead = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="project_lead",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
module_view = models.BooleanField(default=True)
|
||||
cycle_view = models.BooleanField(default=True)
|
||||
issue_views_view = models.BooleanField(default=True)
|
||||
page_view = models.BooleanField(default=True)
|
||||
inbox_view = models.BooleanField(default=False)
|
||||
estimate = models.ForeignKey(
|
||||
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
|
||||
)
|
||||
archive_in = models.IntegerField(
|
||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
)
|
||||
close_in = models.IntegerField(
|
||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
)
|
||||
close_state = models.ForeignKey(
|
||||
"db.State", on_delete=models.SET_NULL, null=True, related_name="close_state"
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.workspace = self.project.workspace
|
||||
super(ProjectSetting, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the project"""
|
||||
return f"{self.project.name} <{self.workspace.name}>"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Project Settings"
|
||||
verbose_name_plural = "Project Settings"
|
||||
db_table = "project_settings"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class ProjectBaseModel(BaseModel):
|
||||
project = models.ForeignKey(
|
||||
Project, on_delete=models.CASCADE, related_name="project_%(class)s"
|
||||
|
||||
@@ -74,7 +74,6 @@ export const AutoArchiveAutomation: React.FC<Props> = ({
|
||||
handleChange({ archive_in: val });
|
||||
}}
|
||||
input
|
||||
verticalPosition="bottom"
|
||||
width="w-full"
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -100,7 +100,6 @@ export const ThemeSwitch: React.FC<Props> = observer(
|
||||
}}
|
||||
input
|
||||
width="w-full"
|
||||
position="right"
|
||||
>
|
||||
{THEMES_OBJ.map(({ value, label, type, icon }) => (
|
||||
<CustomSelect.Option key={value} value={{ value, type }}>
|
||||
|
||||
@@ -255,15 +255,11 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
!isDraftIssuesPage && (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none whitespace-nowrap"
|
||||
>
|
||||
<div className="flex items-center gap-2 font-medium text-custom-primary outline-none whitespace-nowrap">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
position="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
|
||||
@@ -369,12 +369,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{type && !isNotAllowed && (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<button
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded p-1 text-left text-xs duration-300 hover:bg-custom-background-80"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
|
||||
@@ -272,7 +272,6 @@ export const SingleList: React.FC<Props> = (props) => {
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</div>
|
||||
}
|
||||
position="right"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
|
||||
|
||||
@@ -255,7 +255,6 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
<CustomMenu
|
||||
customButtonClassName="!w-full"
|
||||
className="!w-full"
|
||||
position="left"
|
||||
customButton={
|
||||
<div
|
||||
className={`relative group flex items-center justify-between gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-3 px-2 ${
|
||||
@@ -641,16 +640,11 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
<CustomMenu
|
||||
className="sticky left-0 z-10"
|
||||
customButton={
|
||||
<button
|
||||
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full"
|
||||
type="button"
|
||||
>
|
||||
<div className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
New Issue
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
position="left"
|
||||
verticalPosition="top"
|
||||
optionsClassName="left-5 !w-36"
|
||||
noBorder
|
||||
>
|
||||
|
||||
@@ -92,7 +92,6 @@ export const SelectRepository: React.FC<Props> = ({
|
||||
)}
|
||||
</>
|
||||
}
|
||||
position="right"
|
||||
optionsClassName="w-full"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -143,7 +143,6 @@ export const JiraGetImportDetail: React.FC = () => {
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
verticalPosition="top"
|
||||
>
|
||||
{projects && projects.length > 0 ? (
|
||||
projects.map((project) => (
|
||||
|
||||
@@ -158,7 +158,7 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<CustomMenu position="left" ellipsis optionsClassName="px-1.5">
|
||||
<CustomMenu ellipsis optionsClassName="px-1.5">
|
||||
{siblingIssuesList ? (
|
||||
siblingIssuesList.length > 0 ? (
|
||||
<>
|
||||
|
||||
@@ -93,7 +93,6 @@ export const PeekOverviewHeader: React.FC<Props> = ({
|
||||
<Icon iconName={peekModes.find((m) => m.key === mode)?.icon ?? ""} />
|
||||
</button>
|
||||
}
|
||||
position="left"
|
||||
>
|
||||
{peekModes.map((mode) => (
|
||||
<CustomSelect.Option key={mode.key} value={mode.key}>
|
||||
|
||||
@@ -33,7 +33,6 @@ export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
position="right"
|
||||
width="w-full min-w-[8rem]"
|
||||
noChevron
|
||||
>
|
||||
|
||||
@@ -91,7 +91,6 @@ export const SidebarCycleSelect: React.FC<Props> = ({
|
||||
: handleCycleChange(incompleteCycles?.find((c) => c.id === value) as ICycle);
|
||||
}}
|
||||
width="w-full"
|
||||
position="right"
|
||||
maxHeight="rg"
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -20,17 +20,14 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
<CustomSelect
|
||||
value={value}
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 !text-sm bg-custom-background-80 rounded px-2.5 py-0.5"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 !text-sm bg-custom-background-80 rounded px-2.5 py-0.5">
|
||||
<PlayIcon
|
||||
className={`h-4 w-4 -rotate-90 ${
|
||||
value !== null ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
/>
|
||||
{estimatePoints?.find((e) => e.key === value)?.value ?? "No estimate"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -87,7 +87,6 @@ export const SidebarModuleSelect: React.FC<Props> = ({
|
||||
: handleModuleChange(modules?.find((m) => m.id === value) as IModule);
|
||||
}}
|
||||
width="w-full"
|
||||
position="right"
|
||||
maxHeight="rg"
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -18,8 +18,7 @@ type Props = {
|
||||
export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabled = false }) => (
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
<div
|
||||
className={`flex items-center gap-1.5 text-left text-xs capitalize rounded px-2.5 py-0.5 ${
|
||||
value === "urgent"
|
||||
? "border-red-500/20 bg-red-500/20 text-red-500"
|
||||
@@ -36,7 +35,7 @@ export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
<PriorityIcon priority={value} className="!text-sm" />
|
||||
</span>
|
||||
<span>{value ?? "None"}</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled
|
||||
return (
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<button type="button" className="bg-custom-background-80 text-xs rounded px-2.5 py-0.5">
|
||||
<div className="bg-custom-background-80 text-xs rounded px-2.5 py-0.5">
|
||||
{selectedState ? (
|
||||
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
|
||||
<StateGroupIcon stateGroup={selectedState.group} color={selectedState.color} />
|
||||
@@ -53,12 +53,11 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled
|
||||
) : (
|
||||
"None"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
optionsClassName="w-min"
|
||||
position="left"
|
||||
disabled={disabled}
|
||||
>
|
||||
{states ? (
|
||||
|
||||
@@ -247,7 +247,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
|
||||
</>
|
||||
}
|
||||
buttonClassName="whitespace-nowrap"
|
||||
position="left"
|
||||
noBorder
|
||||
noChevron
|
||||
>
|
||||
@@ -283,7 +282,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
|
||||
</>
|
||||
}
|
||||
buttonClassName="whitespace-nowrap"
|
||||
position="left"
|
||||
noBorder
|
||||
noChevron
|
||||
>
|
||||
|
||||
@@ -113,10 +113,8 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
||||
{...(customButton ? { customButton: assigneeLabel } : { label: assigneeLabel })}
|
||||
multiple
|
||||
noChevron
|
||||
position={position}
|
||||
disabled={isNotAllowed}
|
||||
onOpen={() => setFetchAssignees(true)}
|
||||
selfPositioned={selfPositioned}
|
||||
width="w-full min-w-[12rem]"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -74,8 +74,6 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
||||
maxHeight="md"
|
||||
noChevron
|
||||
disabled={isNotAllowed}
|
||||
position={position}
|
||||
selfPositioned={selfPositioned}
|
||||
width="w-full min-w-[8rem]"
|
||||
>
|
||||
<CustomSelect.Option value={null}>
|
||||
|
||||
@@ -146,9 +146,7 @@ export const ViewLabelSelect: React.FC<Props> = ({
|
||||
{...(customButton ? { customButton: labelsLabel } : { label: labelsLabel })}
|
||||
multiple
|
||||
noChevron
|
||||
position={position}
|
||||
disabled={isNotAllowed}
|
||||
selfPositioned={selfPositioned}
|
||||
footerOption={footerOption}
|
||||
width="w-full min-w-[12rem]"
|
||||
/>
|
||||
|
||||
@@ -59,8 +59,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
||||
}}
|
||||
maxHeight="md"
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
<div
|
||||
className={`grid place-items-center rounded ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} ${noBorder ? "" : "h-6 w-6 border shadow-sm"} ${
|
||||
@@ -94,12 +93,10 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
||||
{noBorder ? capitalizeFirstLetter(issue.priority ?? "None") : ""}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
noChevron
|
||||
disabled={isNotAllowed}
|
||||
position={position}
|
||||
selfPositioned={selfPositioned}
|
||||
>
|
||||
{PRIORITIES?.map((priority) => (
|
||||
<CustomSelect.Option key={priority} value={priority} className="capitalize">
|
||||
|
||||
@@ -65,7 +65,6 @@ export const SidebarLeadSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
}
|
||||
options={options}
|
||||
maxHeight="md"
|
||||
position="right"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,6 @@ export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
maxHeight="md"
|
||||
position="right"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -214,12 +214,9 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
|
||||
>
|
||||
<div className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded">
|
||||
<Icon iconName="schedule" className="h-5 w-5 text-custom-text-300" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
optionsClassName="!z-20"
|
||||
>
|
||||
|
||||
@@ -167,7 +167,6 @@ export const UserDetails: React.FC<Props> = ({ user }) => {
|
||||
}
|
||||
input
|
||||
width="w-full"
|
||||
verticalPosition="top"
|
||||
>
|
||||
{USER_ROLES.map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
@@ -197,7 +196,6 @@ export const UserDetails: React.FC<Props> = ({ user }) => {
|
||||
}
|
||||
options={timeZoneOptions}
|
||||
onChange={onChange}
|
||||
verticalPosition="top"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
/>
|
||||
|
||||
@@ -384,12 +384,12 @@ export const SinglePageBlock: React.FC<Props> = ({
|
||||
</button>
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<button
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded px-2.5 py-1 text-left text-xs duration-300 hover:bg-custom-background-90"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<BoltIcon className="h-4.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{block.issue ? (
|
||||
|
||||
@@ -418,7 +418,6 @@ export const CreateProjectModal: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
verticalPosition="top"
|
||||
noChevron
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-popper
|
||||
import { usePopper } from "react-popper";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// headless ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// component
|
||||
@@ -19,6 +19,7 @@ import { PlusIcon } from "lucide-react";
|
||||
// types
|
||||
import { Tooltip } from "components/ui";
|
||||
import { ICurrentUserResponse, IIssueLabels } from "types";
|
||||
import { Placement } from "@popperjs/core";
|
||||
// constants
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
@@ -31,6 +32,7 @@ type Props = {
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
maxRender?: number;
|
||||
placement?: Placement;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
@@ -45,21 +47,25 @@ export const LabelSelect: React.FC<Props> = ({
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
maxRender = 2,
|
||||
placement,
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
user,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [fetchStates, setFetchStates] = useState(false);
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const [labelModal, setLabelModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId) : null,
|
||||
@@ -131,8 +137,6 @@ export const LabelSelect: React.FC<Props> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
const footerOption = (
|
||||
<button
|
||||
type="button"
|
||||
@@ -165,33 +169,35 @@ export const LabelSelect: React.FC<Props> = ({
|
||||
multiple
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
if (open) setFetchStates(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: value.length <= maxRender
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
<Combobox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: value.length <= maxRender
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
|
||||
<Combobox.Options>
|
||||
<div
|
||||
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
@@ -234,8 +240,8 @@ export const LabelSelect: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
{footerOption}
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -77,7 +77,6 @@ export const MemberSelect: React.FC<Props> = ({ value, onChange, isDisabled = fa
|
||||
]
|
||||
}
|
||||
maxHeight="md"
|
||||
position="right"
|
||||
width="w-full"
|
||||
onChange={onChange}
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-popper
|
||||
import { usePopper } from "react-popper";
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
import useProjectMembers from "hooks/use-project-members";
|
||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||
// headless ui
|
||||
@@ -15,6 +16,7 @@ import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IUser } from "types";
|
||||
import { Placement } from "@popperjs/core";
|
||||
|
||||
type Props = {
|
||||
value: string | string[];
|
||||
@@ -25,6 +27,7 @@ type Props = {
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
placement?: Placement;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
@@ -38,18 +41,22 @@ export const MembersSelect: React.FC<Props> = ({
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
placement,
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [fetchStates, setFetchStates] = useState(false);
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
|
||||
const { members } = useProjectMembers(
|
||||
workspaceSlug?.toString(),
|
||||
@@ -105,8 +112,6 @@ export const MembersSelect: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -117,31 +122,32 @@ export const MembersSelect: React.FC<Props> = ({
|
||||
multiple
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
if (open) setFetchStates(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
<Combobox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
<Combobox.Options>
|
||||
<div
|
||||
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
@@ -183,8 +189,8 @@ export const MembersSelect: React.FC<Props> = ({
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// react-popper
|
||||
import { usePopper } from "react-popper";
|
||||
// headless ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// icons
|
||||
@@ -12,6 +12,7 @@ import { PriorityIcon } from "components/icons";
|
||||
import { Tooltip } from "components/ui";
|
||||
// types
|
||||
import { TIssuePriorities } from "types";
|
||||
import { Placement } from "@popperjs/core";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
@@ -21,6 +22,7 @@ type Props = {
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
placement?: Placement;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
@@ -31,14 +33,18 @@ export const PrioritySelect: React.FC<Props> = ({
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
placement,
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
|
||||
const options = PRIORITIES?.map((priority) => ({
|
||||
value: priority,
|
||||
@@ -87,8 +93,6 @@ export const PrioritySelect: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -97,77 +101,71 @@ export const PrioritySelect: React.FC<Props> = ({
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 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 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
<Combobox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
<div
|
||||
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 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 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -230,7 +230,6 @@ const SendProjectInvitationModal: React.FC<Props> = (props) => {
|
||||
onChange(val);
|
||||
}}
|
||||
options={options}
|
||||
position="left"
|
||||
width="w-full min-w-[12rem]"
|
||||
/>
|
||||
)}
|
||||
@@ -252,12 +251,12 @@ const SendProjectInvitationModal: React.FC<Props> = (props) => {
|
||||
<CustomSelect
|
||||
{...field}
|
||||
customButton={
|
||||
<button className="flex w-full items-center justify-between gap-1 rounded-md border border-custom-border-200 shadow-sm duration-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 focus:outline-none px-3 py-2.5 text-sm text-left">
|
||||
<div className="flex w-full items-center justify-between gap-1 rounded-md border border-custom-border-200 shadow-sm duration-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 focus:outline-none px-3 py-2.5 text-sm text-left">
|
||||
<span className="capitalize">
|
||||
{field.value ? ROLE[field.value] : "Select role"}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
input
|
||||
width="w-full"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// react-popper
|
||||
import { usePopper } from "react-popper";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
// headless ui
|
||||
@@ -16,6 +16,7 @@ import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// types
|
||||
import { Tooltip } from "components/ui";
|
||||
import { Placement } from "@popperjs/core";
|
||||
// constants
|
||||
import { IState } from "types";
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
@@ -29,6 +30,7 @@ type Props = {
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
placement?: Placement;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
@@ -40,17 +42,21 @@ export const StateSelect: React.FC<Props> = ({
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
placement,
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const [fetchStates, setFetchStates] = useState<boolean>(false);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
@@ -90,8 +96,6 @@ export const StateSelect: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -103,31 +107,32 @@ export const StateSelect: React.FC<Props> = ({
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
if (open) setFetchStates(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
<Combobox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
<Combobox.Options>
|
||||
<div
|
||||
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
@@ -169,8 +174,8 @@ export const StateSelect: React.FC<Props> = ({
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
// react-poppper
|
||||
import { usePopper } from "react-popper";
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Menu } from "@headlessui/react";
|
||||
// ui
|
||||
import { DropdownProps } from "components/ui";
|
||||
// icons
|
||||
@@ -20,6 +22,7 @@ export type CustomMenuProps = DropdownProps & {
|
||||
const CustomMenu = ({
|
||||
buttonClassName = "",
|
||||
customButtonClassName = "",
|
||||
placement,
|
||||
children,
|
||||
className = "",
|
||||
customButton,
|
||||
@@ -30,100 +33,106 @@ const CustomMenu = ({
|
||||
noBorder = false,
|
||||
noChevron = false,
|
||||
optionsClassName = "",
|
||||
position = "right",
|
||||
selfPositioned = false,
|
||||
verticalEllipsis = false,
|
||||
verticalPosition = "bottom",
|
||||
width = "auto",
|
||||
menuButtonOnClick,
|
||||
}: CustomMenuProps) => (
|
||||
<Menu as="div" className={`${selfPositioned ? "" : "relative"} w-min text-left ${className}`}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{customButton ? (
|
||||
<Menu.Button
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={menuButtonOnClick}
|
||||
className={customButtonClassName}
|
||||
disabled={disabled}
|
||||
>
|
||||
{customButton}
|
||||
</Menu.Button>
|
||||
) : (
|
||||
<>
|
||||
{ellipsis || verticalEllipsis ? (
|
||||
<Menu.Button
|
||||
}: CustomMenuProps) => {
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
return (
|
||||
<Menu as="div" className={`relative w-min text-left ${className}`}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{customButton ? (
|
||||
<Menu.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
onClick={menuButtonOnClick}
|
||||
disabled={disabled}
|
||||
className={`relative grid place-items-center rounded p-1 text-custom-text-200 hover:text-custom-text-100 outline-none ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
className={customButtonClassName}
|
||||
>
|
||||
<MoreHorizOutlined
|
||||
fontSize="small"
|
||||
className={verticalEllipsis ? "rotate-90" : ""}
|
||||
/>
|
||||
</Menu.Button>
|
||||
) : (
|
||||
<Menu.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 rounded-md px-2.5 py-1 text-xs whitespace-nowrap duration-300 ${
|
||||
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
|
||||
} ${
|
||||
noBorder ? "" : "border border-custom-border-300 shadow-sm focus:outline-none"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && (
|
||||
<ExpandMoreOutlined
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</Menu.Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className={`absolute z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 p-1 text-xs shadow-lg focus:outline-none bg-custom-background-90 ${
|
||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
>
|
||||
<div className="py-1">{children}</div>
|
||||
{customButton}
|
||||
</button>
|
||||
</Menu.Button>
|
||||
) : (
|
||||
<>
|
||||
{ellipsis || verticalEllipsis ? (
|
||||
<Menu.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
onClick={menuButtonOnClick}
|
||||
disabled={disabled}
|
||||
className={`relative grid place-items-center rounded p-1 text-custom-text-200 hover:text-custom-text-100 outline-none ${
|
||||
disabled
|
||||
? "cursor-not-allowed"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
<MoreHorizOutlined
|
||||
fontSize="small"
|
||||
className={verticalEllipsis ? "rotate-90" : ""}
|
||||
/>
|
||||
</button>
|
||||
</Menu.Button>
|
||||
) : (
|
||||
<Menu.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 rounded-md px-2.5 py-1 text-xs whitespace-nowrap duration-300 ${
|
||||
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
|
||||
} ${
|
||||
noBorder ? "" : "border border-custom-border-300 shadow-sm focus:outline-none"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && (
|
||||
<ExpandMoreOutlined
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</Menu.Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Menu.Items>
|
||||
<div
|
||||
className={`z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 p-1 text-xs shadow-custom-shadow-rg focus:outline-none bg-custom-background-90 my-1 ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="py-1">{children}</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
type MenuItemProps = {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// react-poppper
|
||||
import { usePopper } from "react-popper";
|
||||
// headless ui
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
@@ -27,9 +29,11 @@ export type CustomSearchSelectProps = DropdownProps & {
|
||||
);
|
||||
|
||||
export const CustomSearchSelect = ({
|
||||
customButtonClassName = "",
|
||||
buttonClassName = "",
|
||||
className = "",
|
||||
customButton,
|
||||
placement,
|
||||
disabled = false,
|
||||
footerOption,
|
||||
input = false,
|
||||
@@ -41,14 +45,18 @@ export const CustomSearchSelect = ({
|
||||
options,
|
||||
onOpen,
|
||||
optionsClassName = "",
|
||||
position = "left",
|
||||
selfPositioned = false,
|
||||
value,
|
||||
verticalPosition = "bottom",
|
||||
width = "auto",
|
||||
}: CustomSearchSelectProps) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
@@ -63,51 +71,54 @@ export const CustomSearchSelect = ({
|
||||
if (multiple) props.multiple = true;
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
|
||||
{...props}
|
||||
>
|
||||
<Combobox as="div" className={`relative flex-shrink-0 text-left ${className}`} {...props}>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open && onOpen) onOpen();
|
||||
|
||||
return (
|
||||
<>
|
||||
{customButton ? (
|
||||
<Combobox.Button as="div">{customButton}</Combobox.Button>
|
||||
<Combobox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
) : (
|
||||
<Combobox.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
<Combobox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none ${
|
||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
)}
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Combobox.Options
|
||||
className={`absolute z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||
<Combobox.Options as={React.Fragment}>
|
||||
<div
|
||||
className={`z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-custom-shadow-rg focus:outline-none my-1 ${
|
||||
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
|
||||
} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" />
|
||||
@@ -176,8 +187,8 @@ export const CustomSearchSelect = ({
|
||||
)}
|
||||
</div>
|
||||
{footerOption}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
// react-popper
|
||||
import { usePopper } from "react-popper";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||
@@ -15,7 +17,9 @@ export type CustomSelectProps = DropdownProps & {
|
||||
};
|
||||
|
||||
const CustomSelect = ({
|
||||
customButtonClassName = "",
|
||||
buttonClassName = "",
|
||||
placement,
|
||||
children,
|
||||
className = "",
|
||||
customButton,
|
||||
@@ -26,68 +30,83 @@ const CustomSelect = ({
|
||||
noChevron = false,
|
||||
onChange,
|
||||
optionsClassName = "",
|
||||
position = "left",
|
||||
selfPositioned = false,
|
||||
value,
|
||||
verticalPosition = "bottom",
|
||||
width = "auto",
|
||||
}: CustomSelectProps) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
|
||||
disabled={disabled}
|
||||
>
|
||||
<>
|
||||
{customButton ? (
|
||||
<Listbox.Button as={React.Fragment}>{customButton}</Listbox.Button>
|
||||
) : (
|
||||
<Listbox.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none ${
|
||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
|
||||
</Listbox.Button>
|
||||
)}
|
||||
</>
|
||||
}: CustomSelectProps) => {
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
|
||||
return (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={`relative flex-shrink-0 text-left ${className}`}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Listbox.Options
|
||||
className={`absolute z-10 border border-custom-border-300 mt-1 origin-top-right overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
>
|
||||
<div className="space-y-1 p-2">{children}</div>
|
||||
<>
|
||||
{customButton ? (
|
||||
<Listbox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
</Listbox.Button>
|
||||
) : (
|
||||
<Listbox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none ${
|
||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</Listbox.Button>
|
||||
)}
|
||||
</>
|
||||
<Listbox.Options>
|
||||
<div
|
||||
className={`z-10 border border-custom-border-300 overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-custom-shadow-rg focus:outline-none my-1 ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="space-y-1 p-2">{children}</div>
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
);
|
||||
</Listbox>
|
||||
);
|
||||
};
|
||||
|
||||
type OptionProps = {
|
||||
children: React.ReactNode;
|
||||
|
||||
+4
-3
@@ -1,4 +1,7 @@
|
||||
import { Placement } from "@popperjs/core";
|
||||
|
||||
export type DropdownProps = {
|
||||
customButtonClassName?: string;
|
||||
buttonClassName?: string;
|
||||
customButtonClassName?: string;
|
||||
className?: string;
|
||||
@@ -10,8 +13,6 @@ export type DropdownProps = {
|
||||
noChevron?: boolean;
|
||||
onOpen?: () => void;
|
||||
optionsClassName?: string;
|
||||
position?: "right" | "left";
|
||||
selfPositioned?: boolean;
|
||||
verticalPosition?: "top" | "bottom";
|
||||
width?: "auto" | string;
|
||||
placement?: Placement;
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@nivo/line": "0.80.0",
|
||||
"@nivo/pie": "0.80.0",
|
||||
"@nivo/scatterplot": "0.80.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/nextjs": "^7.36.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||
"@tiptap/extension-color": "^2.0.4",
|
||||
@@ -72,6 +73,7 @@
|
||||
"react-hook-form": "^7.38.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-moveable": "^0.54.1",
|
||||
"react-popper": "^2.3.0",
|
||||
"sharp": "^0.32.1",
|
||||
"sonner": "^0.6.2",
|
||||
"swr": "^2.1.3",
|
||||
|
||||
@@ -298,8 +298,6 @@ const Profile: NextPage = () => {
|
||||
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""}
|
||||
width="w-full"
|
||||
input
|
||||
verticalPosition="top"
|
||||
position="right"
|
||||
>
|
||||
{USER_ROLES.map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
@@ -362,7 +360,6 @@ const Profile: NextPage = () => {
|
||||
}
|
||||
options={timeZoneOptions}
|
||||
onChange={onChange}
|
||||
verticalPosition="top"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
/>
|
||||
|
||||
@@ -427,13 +427,10 @@ const SinglePage: NextPage = () => {
|
||||
)}
|
||||
<CustomSearchSelect
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded-sm bg-custom-background-80 p-1.5 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-1 rounded-sm bg-custom-background-80 p-1.5 text-xs">
|
||||
<PlusIcon className="h-3.5 w-3.5" />
|
||||
{pageDetails.labels.length <= 0 && <span>Add Label</span>}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
value={pageDetails.labels}
|
||||
footerOption={
|
||||
|
||||
@@ -412,7 +412,7 @@ const MembersSettings: NextPage = () => {
|
||||
)}
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<button className="flex item-center gap-1">
|
||||
<div className="flex item-center gap-1">
|
||||
<span
|
||||
className={`flex items-center text-sm font-medium ${
|
||||
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
|
||||
@@ -423,7 +423,7 @@ const MembersSettings: NextPage = () => {
|
||||
{member.memberId !== user?.id && (
|
||||
<Icon iconName="expand_more" className="text-lg font-medium" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
value={member.role}
|
||||
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
|
||||
@@ -455,7 +455,6 @@ const MembersSettings: NextPage = () => {
|
||||
});
|
||||
});
|
||||
}}
|
||||
position="right"
|
||||
disabled={
|
||||
member.memberId === user?.id ||
|
||||
!member.member ||
|
||||
|
||||
@@ -269,7 +269,7 @@ const MembersSettings: NextPage = () => {
|
||||
)}
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<button className="flex item-center gap-1">
|
||||
<div className="flex item-center gap-1">
|
||||
<span
|
||||
className={`flex items-center text-sm font-medium ${
|
||||
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
|
||||
@@ -280,7 +280,7 @@ const MembersSettings: NextPage = () => {
|
||||
{member.memberId !== user?.id && (
|
||||
<Icon iconName="expand_more" className="text-lg font-medium" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
value={member.role}
|
||||
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
|
||||
@@ -307,7 +307,6 @@ const MembersSettings: NextPage = () => {
|
||||
});
|
||||
});
|
||||
}}
|
||||
position="right"
|
||||
disabled={
|
||||
member.memberId === currentUser?.member.id ||
|
||||
!member.status ||
|
||||
|
||||
@@ -6628,20 +6628,13 @@ prosemirror-menu@^1.2.1:
|
||||
prosemirror-history "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
|
||||
prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1:
|
||||
prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd"
|
||||
integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
prosemirror-model@^1.19.0:
|
||||
version "1.19.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006"
|
||||
integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
prosemirror-schema-basic@^1.2.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7"
|
||||
|
||||
Reference in New Issue
Block a user