forked from github/plane
fix: merge conflicts
This commit is contained in:
commit
501a704108
@ -49,6 +49,7 @@ class IssueFlatSerializer(BaseSerializer):
|
|||||||
"target_date",
|
"target_date",
|
||||||
"sequence_id",
|
"sequence_id",
|
||||||
"sort_order",
|
"sort_order",
|
||||||
|
"is_draft",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -292,12 +293,12 @@ class IssueLabelSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueRelationSerializer(BaseSerializer):
|
class IssueRelationSerializer(BaseSerializer):
|
||||||
related_issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueRelation
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"related_issue_detail",
|
"issue_detail",
|
||||||
"relation_type",
|
"relation_type",
|
||||||
"related_issue",
|
"related_issue",
|
||||||
"issue",
|
"issue",
|
||||||
|
@ -1038,6 +1038,7 @@ urlpatterns = [
|
|||||||
IssueDraftViewSet.as_view(
|
IssueDraftViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "list",
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-issue-draft",
|
name="project-issue-draft",
|
||||||
@ -1047,6 +1048,7 @@ urlpatterns = [
|
|||||||
IssueDraftViewSet.as_view(
|
IssueDraftViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
"delete": "destroy",
|
"delete": "destroy",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -53,6 +53,7 @@ from plane.api.serializers import (
|
|||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueVoteSerializer,
|
IssueVoteSerializer,
|
||||||
IssueRelationSerializer,
|
IssueRelationSerializer,
|
||||||
|
RelatedIssueSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
@ -508,7 +509,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||||||
issue_activities = (
|
issue_activities = (
|
||||||
IssueActivity.objects.filter(issue_id=issue_id)
|
IssueActivity.objects.filter(issue_id=issue_id)
|
||||||
.filter(
|
.filter(
|
||||||
~Q(field__in=["comment", "vote", "reaction"]),
|
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||||
project__project_projectmember__member=self.request.user,
|
project__project_projectmember__member=self.request.user,
|
||||||
)
|
)
|
||||||
.select_related("actor", "workspace", "issue", "project")
|
.select_related("actor", "workspace", "issue", "project")
|
||||||
@ -2085,9 +2086,10 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
def create(self, request, slug, project_id, issue_id):
|
def create(self, request, slug, project_id, issue_id):
|
||||||
try:
|
try:
|
||||||
related_list = request.data.get("related_list", [])
|
related_list = request.data.get("related_list", [])
|
||||||
|
relation = request.data.get("relation", None)
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
issueRelation = IssueRelation.objects.bulk_create(
|
issue_relation = IssueRelation.objects.bulk_create(
|
||||||
[
|
[
|
||||||
IssueRelation(
|
IssueRelation(
|
||||||
issue_id=related_issue["issue"],
|
issue_id=related_issue["issue"],
|
||||||
@ -2112,11 +2114,17 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
if relation == "blocking":
|
||||||
IssueRelationSerializer(issueRelation, many=True).data,
|
return Response(
|
||||||
status=status.HTTP_201_CREATED,
|
RelatedIssueSerializer(issue_relation, many=True).data,
|
||||||
)
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
IssueRelationSerializer(issue_relation, many=True).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
return Response(
|
return Response(
|
||||||
@ -2358,6 +2366,47 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
serializer_class = IssueFlatSerializer
|
serializer_class = IssueFlatSerializer
|
||||||
model = Issue
|
model = Issue
|
||||||
|
|
||||||
|
|
||||||
|
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_draft.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
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
current_instance = (
|
||||||
|
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||||
|
)
|
||||||
|
if current_instance is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.deleted",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
{"issue_id": str(self.kwargs.get("pk", None))}
|
||||||
|
),
|
||||||
|
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
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.objects.annotate(
|
Issue.objects.annotate(
|
||||||
@ -2383,6 +2432,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
@ -2492,6 +2542,40 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
serializer = IssueCreateSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={
|
||||||
|
"project_id": project_id,
|
||||||
|
"workspace_id": project.workspace_id,
|
||||||
|
"default_assignee_id": project.default_assignee_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(is_draft=True)
|
||||||
|
|
||||||
|
# Track the issue
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.created",
|
||||||
|
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
except Project.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
try:
|
try:
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
|
@ -396,16 +396,16 @@ def track_assignees(
|
|||||||
def create_issue_activity(
|
def create_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"created the issue",
|
comment=f"created the issue",
|
||||||
verb="created",
|
verb="created",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def track_estimate_points(
|
def track_estimate_points(
|
||||||
@ -518,11 +518,6 @@ def update_issue_activity(
|
|||||||
"closed_to": track_closed_to,
|
"closed_to": track_closed_to,
|
||||||
}
|
}
|
||||||
|
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
|
||||||
current_instance = (
|
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
|
||||||
)
|
|
||||||
|
|
||||||
for key in requested_data:
|
for key in requested_data:
|
||||||
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
||||||
if func is not None:
|
if func is not None:
|
||||||
@ -1053,6 +1048,25 @@ def create_issue_relation_activity(
|
|||||||
)
|
)
|
||||||
if current_instance is None and requested_data.get("related_list") is not None:
|
if current_instance is None and requested_data.get("related_list") is not None:
|
||||||
for issue_relation in requested_data.get("related_list"):
|
for issue_relation in requested_data.get("related_list"):
|
||||||
|
if issue_relation.get("relation_type") == "blocked_by":
|
||||||
|
relation_type = "blocking"
|
||||||
|
else:
|
||||||
|
relation_type = issue_relation.get("relation_type")
|
||||||
|
issue = Issue.objects.get(pk=issue_relation.get("issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_relation.get("related_issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="created",
|
||||||
|
old_value="",
|
||||||
|
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
field=relation_type,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'added {relation_type} relation',
|
||||||
|
old_identifier=issue_relation.get("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
|
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -1065,7 +1079,7 @@ def create_issue_relation_activity(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f'added {issue_relation.get("relation_type")} relation',
|
comment=f'added {issue_relation.get("relation_type")} relation',
|
||||||
old_identifier=issue_relation.get("issue"),
|
old_identifier=issue_relation.get("related_issue"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1078,23 +1092,105 @@ def delete_issue_relation_activity(
|
|||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
)
|
)
|
||||||
if current_instance is not None and requested_data.get("related_list") is None:
|
if current_instance is not None and requested_data.get("related_list") is None:
|
||||||
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
if current_instance.get("relation_type") == "blocked_by":
|
||||||
|
relation_type = "blocking"
|
||||||
|
else:
|
||||||
|
relation_type = current_instance.get("relation_type")
|
||||||
|
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=current_instance.get("related_issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="deleted",
|
||||||
|
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
new_value="",
|
||||||
|
field=relation_type,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'deleted {relation_type} relation',
|
||||||
|
old_identifier=current_instance.get("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=current_instance.get("issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="deleted",
|
||||||
|
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
new_value="",
|
||||||
|
field=f'{current_instance.get("relation_type")}',
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'deleted {current_instance.get("relation_type")} relation',
|
||||||
|
old_identifier=current_instance.get("related_issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=current_instance.get("issue"),
|
issue_id=issue_id,
|
||||||
actor=actor,
|
|
||||||
verb="deleted",
|
|
||||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
|
||||||
new_value="",
|
|
||||||
field=f'{current_instance.get("relation_type")}',
|
|
||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f'deleted the {current_instance.get("relation_type")} relation',
|
comment=f"drafted the issue",
|
||||||
old_identifier=current_instance.get("issue"),
|
field="draft",
|
||||||
|
verb="created",
|
||||||
|
actor=actor,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
if requested_data.get("is_draft") is not None and requested_data.get("is_draft") == False:
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"created the issue",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"updated the draft issue",
|
||||||
|
field="draft",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def delete_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"deleted the draft issue",
|
||||||
|
field="draft",
|
||||||
|
verb="deleted",
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Receive message from room group
|
# Receive message from room group
|
||||||
@shared_task
|
@shared_task
|
||||||
def issue_activity(
|
def issue_activity(
|
||||||
@ -1166,6 +1262,9 @@ def issue_activity(
|
|||||||
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
|
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
|
||||||
"issue_vote.activity.created": create_issue_vote_activity,
|
"issue_vote.activity.created": create_issue_vote_activity,
|
||||||
"issue_vote.activity.deleted": delete_issue_vote_activity,
|
"issue_vote.activity.deleted": delete_issue_vote_activity,
|
||||||
|
"issue_draft.activity.created": create_draft_issue_activity,
|
||||||
|
"issue_draft.activity.updated": update_draft_issue_activity,
|
||||||
|
"issue_draft.activity.deleted": delete_draft_issue_activity,
|
||||||
}
|
}
|
||||||
|
|
||||||
func = ACTIVITY_MAPPER.get(type)
|
func = ACTIVITY_MAPPER.get(type)
|
||||||
|
23
apiserver/plane/db/migrations/0045_auto_20230915_0655.py
Normal file
23
apiserver/plane/db/migrations/0045_auto_20230915_0655.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
def update_issue_activity(apps, schema_editor):
|
||||||
|
IssueActivityModel = apps.get_model("db", "IssueActivity")
|
||||||
|
updated_issue_activity = []
|
||||||
|
for obj in IssueActivityModel.objects.all():
|
||||||
|
if obj.field == "blocks":
|
||||||
|
obj.field = "blocked_by"
|
||||||
|
updated_issue_activity.append(obj)
|
||||||
|
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0044_auto_20230913_0709'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(update_issue_activity),
|
||||||
|
]
|
@ -39,14 +39,90 @@ def group_results(results_data, group_by, sub_group_by=False):
|
|||||||
|
|
||||||
for value in results_data:
|
for value in results_data:
|
||||||
main_group_attribute = resolve_keys(sub_group_by, value)
|
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||||
if str(main_group_attribute) not in main_responsive_dict:
|
|
||||||
main_responsive_dict[str(main_group_attribute)] = {}
|
|
||||||
group_attribute = resolve_keys(group_by, value)
|
group_attribute = resolve_keys(group_by, value)
|
||||||
if str(group_attribute) in main_responsive_dict:
|
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
|
||||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
if len(main_group_attribute):
|
||||||
|
for attrib in main_group_attribute:
|
||||||
|
if str(attrib) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(attrib)] = {}
|
||||||
|
if str(group_attribute) in main_responsive_dict[str(attrib)]:
|
||||||
|
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(attrib)][str(group_attribute)] = []
|
||||||
|
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(None)] = {}
|
||||||
|
|
||||||
|
if str(group_attribute) in main_responsive_dict[str(None)]:
|
||||||
|
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(None)][str(group_attribute)] = []
|
||||||
|
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||||
|
|
||||||
|
elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list):
|
||||||
|
if str(main_group_attribute) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(main_group_attribute)] = {}
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in main_responsive_dict[str(main_group_attribute)]:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(attrib)] = []
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in main_responsive_dict[str(main_group_attribute)]:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(None)] = []
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||||
|
|
||||||
|
elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list):
|
||||||
|
if len(main_group_attribute):
|
||||||
|
for main_attrib in main_group_attribute:
|
||||||
|
if str(main_attrib) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(main_attrib)] = {}
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in main_responsive_dict[str(main_attrib)]:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(attrib)] = []
|
||||||
|
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in main_responsive_dict[str(main_attrib)]:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(None)] = []
|
||||||
|
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(None)] = {}
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in main_responsive_dict[str(None)]:
|
||||||
|
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(None)][str(attrib)] = []
|
||||||
|
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in main_responsive_dict[str(None)]:
|
||||||
|
main_responsive_dict[str(None)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(None)][str(None)] = []
|
||||||
|
main_responsive_dict[str(None)][str(None)].append(value)
|
||||||
else:
|
else:
|
||||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
|
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
group_attribute = resolve_keys(group_by, value)
|
||||||
|
|
||||||
|
if str(main_group_attribute) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(main_group_attribute)] = {}
|
||||||
|
|
||||||
|
if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||||
|
|
||||||
return main_responsive_dict
|
return main_responsive_dict
|
||||||
|
|
||||||
|
@ -1,30 +1,29 @@
|
|||||||
events { }
|
events { }
|
||||||
|
|
||||||
|
|
||||||
http {
|
http {
|
||||||
sendfile on;
|
sendfile on;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
root /www/data/;
|
root /www/data/;
|
||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
|
|
||||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://planefrontend:3000/;
|
proxy_pass http://planefrontend:3000/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://planebackend:8000/api/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /spaces/ {
|
||||||
|
proxy_pass http://planedeploy:3000/spaces/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /${BUCKET_NAME}/ {
|
||||||
|
proxy_pass http://plane-minio:9000/uploads/;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://planebackend:8000/api/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /spaces/ {
|
|
||||||
proxy_pass http://planedeploy:3000/spaces/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /${BUCKET_NAME}/ {
|
|
||||||
proxy_pass http://plane-minio:9000/uploads/;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,6 +1,4 @@
|
|||||||
# Public boards deploy URL
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL=""
|
|
||||||
# Google Client ID for Google OAuth
|
# Google Client ID for Google OAuth
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
||||||
# Flag to toggle OAuth
|
# Flag to toggle OAuth
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=1
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// components
|
// components
|
||||||
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
||||||
// images
|
// images
|
||||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces" : "";
|
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||||
|
|
||||||
export const SignInView = observer(() => {
|
export const SignInView = observer(() => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
@ -1,2 +1 @@
|
|||||||
export const API_BASE_URL =
|
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
|
||||||
process.env.NEXT_PUBLIC_API_BASE_URL !== undefined ? process.env.NEXT_PUBLIC_API_BASE_URL : "http://localhost:8000";
|
|
||||||
|
@ -5,7 +5,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
// components
|
// components
|
||||||
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
||||||
|
|
||||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces" : "";
|
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||||
|
|
||||||
const OnBoardingPage = () => {
|
const OnBoardingPage = () => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
17
turbo.json
17
turbo.json
@ -15,17 +15,20 @@
|
|||||||
"NEXT_PUBLIC_UNSPLASH_ACCESS",
|
"NEXT_PUBLIC_UNSPLASH_ACCESS",
|
||||||
"NEXT_PUBLIC_UNSPLASH_ENABLED",
|
"NEXT_PUBLIC_UNSPLASH_ENABLED",
|
||||||
"NEXT_PUBLIC_TRACK_EVENTS",
|
"NEXT_PUBLIC_TRACK_EVENTS",
|
||||||
"TRACKER_ACCESS_KEY",
|
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
||||||
"NEXT_PUBLIC_CRISP_ID",
|
"NEXT_PUBLIC_CRISP_ID",
|
||||||
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
|
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
|
||||||
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
|
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
|
||||||
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
|
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
|
||||||
"NEXT_PUBLIC_SLACK_CLIENT_ID",
|
"NEXT_PUBLIC_DEPLOY_WITH_NGINX",
|
||||||
"NEXT_PUBLIC_SLACK_CLIENT_SECRET",
|
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||||
"NEXT_PUBLIC_SUPABASE_URL",
|
"NEXT_PUBLIC_POSTHOG_HOST",
|
||||||
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
"SLACK_OAUTH_URL",
|
||||||
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
"SLACK_CLIENT_ID",
|
||||||
"NEXT_PUBLIC_DEPLOY_WITH_NGINX"
|
"SLACK_CLIENT_SECRET",
|
||||||
|
"JITSU_TRACKER_ACCESS_KEY",
|
||||||
|
"JITSU_TRACKER_HOST",
|
||||||
|
"UNSPLASH_ACCESS_KEY"
|
||||||
],
|
],
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"build": {
|
"build": {
|
||||||
|
@ -103,8 +103,8 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
|
|
||||||
{projectDetails?.close_in !== 0 && (
|
{projectDetails?.close_in !== 0 && (
|
||||||
<div className="ml-12">
|
<div className="ml-12">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col rounded bg-custom-background-90 border border-custom-border-200 p-2">
|
||||||
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
|
<div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
|
||||||
<div className="w-1/2 text-sm font-medium">
|
<div className="w-1/2 text-sm font-medium">
|
||||||
Auto-close issues that are inactive for
|
Auto-close issues that are inactive for
|
||||||
</div>
|
</div>
|
||||||
@ -138,7 +138,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
|
<div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
|
||||||
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
|
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
|
||||||
<div className="w-1/2 ">
|
<div className="w-1/2 ">
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
|
@ -90,14 +90,14 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
archived_at: {
|
archived_at: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
if (activity.new_value === "restore") return "restored the issue.";
|
if (activity.new_value === "restore") return "restored the issue.";
|
||||||
else return "archived the issue.";
|
else return "archived the issue.";
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -136,7 +136,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
blocking: {
|
blocking: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
@ -224,7 +224,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -239,7 +239,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
estimate_point: {
|
estimate_point: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -271,14 +271,14 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
issue: {
|
issue: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
if (activity.verb === "created") return "created the issue.";
|
if (activity.verb === "created") return "created the issue.";
|
||||||
else return "deleted an issue.";
|
else return "deleted an issue.";
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -327,7 +327,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -398,7 +398,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
message: (activity, showIssue, workspaceSlug) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
@ -448,7 +448,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -463,7 +463,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
parent: {
|
parent: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -496,7 +496,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
priority: {
|
priority: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -514,7 +514,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
start_date: {
|
start_date: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -548,7 +548,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -564,7 +564,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />,
|
icon: <Squares2X2Icon className="h-6 w-6 text-custom-sidebar-200" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -598,7 +598,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -52,10 +52,22 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
Icon: FormatListBulletedOutlined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "kanban",
|
||||||
|
Icon: GridViewOutlined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const IssuesFilterView: React.FC = () => {
|
export const IssuesFilterView: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
const isDraftIssues = router.pathname.includes("draft-issues");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
displayFilters,
|
displayFilters,
|
||||||
@ -75,7 +87,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!isArchivedIssues && (
|
{!isArchivedIssues && !isDraftIssues && (
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
{issueViewOptions.map((option) => (
|
{issueViewOptions.map((option) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -105,6 +117,36 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isDraftIssues && (
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
{issueViewForDraftIssues.map((option) => (
|
||||||
|
<Tooltip
|
||||||
|
key={option.type}
|
||||||
|
tooltipContent={
|
||||||
|
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||||
|
}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||||
|
displayFilters.layout === option.type
|
||||||
|
? "bg-custom-sidebar-background-80"
|
||||||
|
: "text-custom-sidebar-text-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => setDisplayFilters({ layout: option.type })}
|
||||||
|
>
|
||||||
|
<option.Icon
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
className={option.type === "gantt_chart" ? "rotate-90" : ""}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<SelectFilters
|
<SelectFilters
|
||||||
filters={filters}
|
filters={filters}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
import NextImage from "next/image";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react-dropzone
|
// react-dropzone
|
||||||
@ -12,7 +11,7 @@ import fileServices from "services/file.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { DangerButton, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { UserCircleIcon } from "components/icons";
|
import { UserCircleIcon } from "components/icons";
|
||||||
|
|
||||||
@ -21,6 +20,8 @@ type Props = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onSuccess: (url: string) => void;
|
onSuccess: (url: string) => void;
|
||||||
|
isRemoving: boolean;
|
||||||
|
handleDelete: () => void;
|
||||||
userImage?: boolean;
|
userImage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,6 +30,8 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
isRemoving,
|
||||||
|
handleDelete,
|
||||||
userImage,
|
userImage,
|
||||||
}) => {
|
}) => {
|
||||||
const [image, setImage] = useState<File | null>(null);
|
const [image, setImage] = useState<File | null>(null);
|
||||||
@ -148,12 +151,10 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<NextImage
|
<img
|
||||||
layout="fill"
|
|
||||||
objectFit="cover"
|
|
||||||
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
||||||
alt="image"
|
alt="image"
|
||||||
className="rounded-lg"
|
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -182,15 +183,22 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
<p className="my-4 text-custom-text-200 text-sm">
|
<p className="my-4 text-custom-text-200 text-sm">
|
||||||
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<div className="flex items-center">
|
||||||
<PrimaryButton
|
<DangerButton onClick={handleDelete} outline disabled={!value}>
|
||||||
onClick={handleSubmit}
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
disabled={!image}
|
</DangerButton>
|
||||||
loading={isImageUploading}
|
</div>
|
||||||
>
|
<div className="flex items-center gap-2">
|
||||||
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
</PrimaryButton>
|
<PrimaryButton
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!image}
|
||||||
|
loading={isImageUploading}
|
||||||
|
>
|
||||||
|
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
@ -50,6 +50,7 @@ type Props = {
|
|||||||
secondaryButton?: React.ReactNode;
|
secondaryButton?: React.ReactNode;
|
||||||
};
|
};
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||||
handleOnDragEnd: (result: DropResult) => Promise<void>;
|
handleOnDragEnd: (result: DropResult) => Promise<void>;
|
||||||
openIssuesListModal: (() => void) | null;
|
openIssuesListModal: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
@ -66,6 +67,7 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
dragDisabled = false,
|
dragDisabled = false,
|
||||||
emptyState,
|
emptyState,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
|
handleDraftIssueAction,
|
||||||
handleOnDragEnd,
|
handleOnDragEnd,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
@ -132,6 +134,7 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
states={states}
|
states={states}
|
||||||
addIssueToGroup={addIssueToGroup}
|
addIssueToGroup={addIssueToGroup}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
handleDraftIssueAction={handleDraftIssueAction}
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
removeIssue={removeIssue}
|
removeIssue={removeIssue}
|
||||||
myIssueProjectId={myIssueProjectId}
|
myIssueProjectId={myIssueProjectId}
|
||||||
@ -149,6 +152,7 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
disableAddIssueOption={disableAddIssueOption}
|
disableAddIssueOption={disableAddIssueOption}
|
||||||
dragDisabled={dragDisabled}
|
dragDisabled={dragDisabled}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
handleDraftIssueAction={handleDraftIssueAction}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
myIssueProjectId={myIssueProjectId}
|
myIssueProjectId={myIssueProjectId}
|
||||||
|
@ -20,6 +20,7 @@ type Props = {
|
|||||||
disableAddIssueOption?: boolean;
|
disableAddIssueOption?: boolean;
|
||||||
dragDisabled: boolean;
|
dragDisabled: boolean;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
@ -37,6 +38,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
disableAddIssueOption = false,
|
disableAddIssueOption = false,
|
||||||
dragDisabled,
|
dragDisabled,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
|
handleDraftIssueAction,
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
myIssueProjectId,
|
myIssueProjectId,
|
||||||
@ -94,6 +96,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
dragDisabled={dragDisabled}
|
dragDisabled={dragDisabled}
|
||||||
groupTitle={singleGroup}
|
groupTitle={singleGroup}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
handleDraftIssueAction={handleDraftIssueAction}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
openIssuesListModal={openIssuesListModal ?? null}
|
openIssuesListModal={openIssuesListModal ?? null}
|
||||||
handleMyIssueOpen={handleMyIssueOpen}
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
|
@ -24,6 +24,7 @@ type Props = {
|
|||||||
dragDisabled: boolean;
|
dragDisabled: boolean;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
@ -41,6 +42,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
disableAddIssueOption = false,
|
disableAddIssueOption = false,
|
||||||
dragDisabled,
|
dragDisabled,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
|
handleDraftIssueAction,
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
handleMyIssueOpen,
|
handleMyIssueOpen,
|
||||||
@ -136,6 +138,16 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
editIssue={() => handleIssueAction(issue, "edit")}
|
editIssue={() => handleIssueAction(issue, "edit")}
|
||||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
|
handleDraftIssueEdit={
|
||||||
|
handleDraftIssueAction
|
||||||
|
? () => handleDraftIssueAction(issue, "edit")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
handleDraftIssueDelete={() =>
|
||||||
|
handleDraftIssueAction
|
||||||
|
? handleDraftIssueAction(issue, "delete")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
handleMyIssueOpen={handleMyIssueOpen}
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
@ -155,7 +167,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
|
display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{provided.placeholder}
|
<>{provided.placeholder}</>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{displayFilters?.group_by !== "created_by" && (
|
{displayFilters?.group_by !== "created_by" && (
|
||||||
|
@ -60,6 +60,8 @@ type Props = {
|
|||||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
|
handleDraftIssueEdit?: () => void;
|
||||||
|
handleDraftIssueDelete?: () => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
@ -79,6 +81,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
removeIssue,
|
removeIssue,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
|
handleDraftIssueEdit,
|
||||||
|
handleDraftIssueDelete,
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
@ -99,6 +103,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const isDraftIssue = router.pathname.includes("draft-issues");
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
@ -211,29 +217,47 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{!isNotAllowed && (
|
{!isNotAllowed && (
|
||||||
<>
|
<>
|
||||||
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
|
<ContextMenu.Item
|
||||||
|
Icon={PencilIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||||
|
else editIssue();
|
||||||
|
}}
|
||||||
|
>
|
||||||
Edit issue
|
Edit issue
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
{!isDraftIssue && (
|
||||||
Make a copy...
|
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
||||||
</ContextMenu.Item>
|
Make a copy...
|
||||||
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
|
</ContextMenu.Item>
|
||||||
|
)}
|
||||||
|
<ContextMenu.Item
|
||||||
|
Icon={TrashIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
|
||||||
|
else handleDeleteIssue(issue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Delete issue
|
Delete issue
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
{!isDraftIssue && (
|
||||||
Copy issue link
|
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||||
</ContextMenu.Item>
|
Copy issue link
|
||||||
<a
|
|
||||||
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
>
|
|
||||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
|
||||||
Open issue in new tab
|
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
</a>
|
)}
|
||||||
|
{!isDraftIssue && (
|
||||||
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||||
|
Open issue in new tab
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div
|
<div
|
||||||
className={`mb-3 rounded bg-custom-background-100 shadow ${
|
className={`mb-3 rounded bg-custom-background-100 shadow ${
|
||||||
@ -268,13 +292,18 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={editIssue}>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||||
|
else editIssue();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<PencilIcon className="h-4 w-4" />
|
<PencilIcon className="h-4 w-4" />
|
||||||
<span>Edit issue</span>
|
<span>Edit issue</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{type !== "issue" && removeIssue && (
|
{type !== "issue" && removeIssue && !isDraftIssue && (
|
||||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<XMarkIcon className="h-4 w-4" />
|
<XMarkIcon className="h-4 w-4" />
|
||||||
@ -282,18 +311,25 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
|
||||||
|
else handleDeleteIssue(issue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<TrashIcon className="h-4 w-4" />
|
<TrashIcon className="h-4 w-4" />
|
||||||
<span>Delete issue</span>
|
<span>Delete issue</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
{!isDraftIssue && (
|
||||||
<div className="flex items-center justify-start gap-2">
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
<LinkIcon className="h-4 w-4" />
|
<div className="flex items-center justify-start gap-2">
|
||||||
<span>Copy issue Link</span>
|
<LinkIcon className="h-4 w-4" />
|
||||||
</div>
|
<span>Copy issue Link</span>
|
||||||
</CustomMenu.MenuItem>
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -308,7 +344,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-sm text-left break-words line-clamp-2"
|
className="text-sm text-left break-words line-clamp-2"
|
||||||
onClick={openPeekOverview}
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||||
|
else openPeekOverview();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{issue.name}
|
{issue.name}
|
||||||
</button>
|
</button>
|
||||||
|
@ -19,7 +19,13 @@ import useIssuesProperties from "hooks/use-issue-properties";
|
|||||||
import useProjectMembers from "hooks/use-project-members";
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
// components
|
// components
|
||||||
import { FiltersList, AllViews } from "components/core";
|
import { FiltersList, AllViews } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal, IssuePeekOverview } from "components/issues";
|
import {
|
||||||
|
CreateUpdateIssueModal,
|
||||||
|
DeleteIssueModal,
|
||||||
|
DeleteDraftIssueModal,
|
||||||
|
IssuePeekOverview,
|
||||||
|
CreateUpdateDraftIssueModal,
|
||||||
|
} from "components/issues";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
@ -70,8 +76,13 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
// trash box
|
// trash box
|
||||||
const [trashBox, setTrashBox] = useState(false);
|
const [trashBox, setTrashBox] = useState(false);
|
||||||
|
|
||||||
|
// selected draft issue
|
||||||
|
const [selectedDraftIssue, setSelectedDraftIssue] = useState<IIssue | null>(null);
|
||||||
|
const [selectedDraftForDelete, setSelectDraftForDelete] = useState<IIssue | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
const isDraftIssues = router.asPath.includes("draft-issues");
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
|
|
||||||
@ -106,6 +117,9 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
[setDeleteIssueModal, setIssueToDelete]
|
[setDeleteIssueModal, setIssueToDelete]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDraftIssueClick = useCallback((issue: any) => setSelectedDraftIssue(issue), []);
|
||||||
|
const handleDraftIssueDelete = useCallback((issue: any) => setSelectDraftForDelete(issue), []);
|
||||||
|
|
||||||
const handleOnDragEnd = useCallback(
|
const handleOnDragEnd = useCallback(
|
||||||
async (result: DropResult) => {
|
async (result: DropResult) => {
|
||||||
setTrashBox(false);
|
setTrashBox(false);
|
||||||
@ -343,6 +357,14 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDraftIssueAction = useCallback(
|
||||||
|
(issue: IIssue, action: "edit" | "delete") => {
|
||||||
|
if (action === "edit") handleDraftIssueClick(issue);
|
||||||
|
else if (action === "delete") handleDraftIssueDelete(issue);
|
||||||
|
},
|
||||||
|
[handleDraftIssueClick, handleDraftIssueDelete]
|
||||||
|
);
|
||||||
|
|
||||||
const removeIssueFromCycle = useCallback(
|
const removeIssueFromCycle = useCallback(
|
||||||
(bridgeId: string, issueId: string) => {
|
(bridgeId: string, issueId: string) => {
|
||||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||||
@ -451,6 +473,27 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
...preloadedData,
|
...preloadedData,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<CreateUpdateDraftIssueModal
|
||||||
|
isOpen={selectedDraftIssue !== null}
|
||||||
|
handleClose={() => setSelectedDraftIssue(null)}
|
||||||
|
data={
|
||||||
|
selectedDraftIssue
|
||||||
|
? {
|
||||||
|
...selectedDraftIssue,
|
||||||
|
is_draft: true,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
fieldsToShow={[
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"label",
|
||||||
|
"assignee",
|
||||||
|
"priority",
|
||||||
|
"dueDate",
|
||||||
|
"priority",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
||||||
handleClose={() => setEditIssueModal(false)}
|
handleClose={() => setEditIssueModal(false)}
|
||||||
@ -462,6 +505,11 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
data={issueToDelete}
|
data={issueToDelete}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
|
<DeleteDraftIssueModal
|
||||||
|
data={selectedDraftForDelete}
|
||||||
|
isOpen={selectedDraftForDelete !== null}
|
||||||
|
handleClose={() => setSelectDraftForDelete(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
{areFiltersApplied && (
|
{areFiltersApplied && (
|
||||||
<>
|
<>
|
||||||
@ -518,23 +566,28 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
displayFilters.group_by === "assignees"
|
displayFilters.group_by === "assignees"
|
||||||
}
|
}
|
||||||
emptyState={{
|
emptyState={{
|
||||||
title: cycleId
|
title: isDraftIssues
|
||||||
|
? "Draft issues will appear here"
|
||||||
|
: cycleId
|
||||||
? "Cycle issues will appear here"
|
? "Cycle issues will appear here"
|
||||||
: moduleId
|
: moduleId
|
||||||
? "Module issues will appear here"
|
? "Module issues will appear here"
|
||||||
: "Project issues will appear here",
|
: "Project issues will appear here",
|
||||||
description:
|
description: isDraftIssues
|
||||||
"Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done.",
|
? "Draft issues are issues that are not yet created."
|
||||||
primaryButton: {
|
: "Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done.",
|
||||||
icon: <PlusIcon className="h-4 w-4" />,
|
primaryButton: !isDraftIssues
|
||||||
text: "New Issue",
|
? {
|
||||||
onClick: () => {
|
icon: <PlusIcon className="h-4 w-4" />,
|
||||||
const e = new KeyboardEvent("keydown", {
|
text: "New Issue",
|
||||||
key: "c",
|
onClick: () => {
|
||||||
});
|
const e = new KeyboardEvent("keydown", {
|
||||||
document.dispatchEvent(e);
|
key: "c",
|
||||||
},
|
});
|
||||||
},
|
document.dispatchEvent(e);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
secondaryButton:
|
secondaryButton:
|
||||||
cycleId || moduleId ? (
|
cycleId || moduleId ? (
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
@ -548,6 +601,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
handleOnDragEnd={handleOnDragEnd}
|
handleOnDragEnd={handleOnDragEnd}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
handleDraftIssueAction={handleDraftIssueAction}
|
||||||
openIssuesListModal={openIssuesListModal ?? null}
|
openIssuesListModal={openIssuesListModal ?? null}
|
||||||
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
|
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
|
||||||
trashBox={trashBox}
|
trashBox={trashBox}
|
||||||
|
@ -15,6 +15,7 @@ type Props = {
|
|||||||
states: IState[] | undefined;
|
states: IState[] | undefined;
|
||||||
addIssueToGroup: (groupTitle: string) => void;
|
addIssueToGroup: (groupTitle: string) => void;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
myIssueProjectId?: string | null;
|
myIssueProjectId?: string | null;
|
||||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
@ -36,6 +37,7 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
myIssueProjectId,
|
myIssueProjectId,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
states,
|
states,
|
||||||
|
handleDraftIssueAction,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
viewProps,
|
viewProps,
|
||||||
@ -82,6 +84,7 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
groupTitle={singleGroup}
|
groupTitle={singleGroup}
|
||||||
currentState={currentState}
|
currentState={currentState}
|
||||||
addIssueToGroup={() => addIssueToGroup(singleGroup)}
|
addIssueToGroup={() => addIssueToGroup(singleGroup)}
|
||||||
|
handleDraftIssueAction={handleDraftIssueAction}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
handleMyIssueOpen={handleMyIssueOpen}
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
openIssuesListModal={openIssuesListModal}
|
openIssuesListModal={openIssuesListModal}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
@ -18,6 +17,7 @@ import {
|
|||||||
ViewPrioritySelect,
|
ViewPrioritySelect,
|
||||||
ViewStartDateSelect,
|
ViewStartDateSelect,
|
||||||
ViewStateSelect,
|
ViewStateSelect,
|
||||||
|
CreateUpdateDraftIssueModal,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||||
@ -61,6 +61,8 @@ type Props = {
|
|||||||
makeIssueCopy: () => void;
|
makeIssueCopy: () => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
|
handleDraftIssueSelect?: (issue: IIssue) => void;
|
||||||
|
handleDraftIssueDelete?: (issue: IIssue) => void;
|
||||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
@ -76,12 +78,14 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
|
handleDraftIssueDelete,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
handleMyIssueOpen,
|
handleMyIssueOpen,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
viewProps,
|
viewProps,
|
||||||
|
handleDraftIssueSelect,
|
||||||
}) => {
|
}) => {
|
||||||
// context menu
|
// context menu
|
||||||
const [contextMenu, setContextMenu] = useState(false);
|
const [contextMenu, setContextMenu] = useState(false);
|
||||||
@ -90,6 +94,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -178,6 +183,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const issuePath = isArchivedIssues
|
const issuePath = isArchivedIssues
|
||||||
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
||||||
|
: isDraftIssues
|
||||||
|
? `#`
|
||||||
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
|
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
|
||||||
|
|
||||||
const openPeekOverview = (issue: IIssue) => {
|
const openPeekOverview = (issue: IIssue) => {
|
||||||
@ -203,26 +210,45 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{!isNotAllowed && (
|
{!isNotAllowed && (
|
||||||
<>
|
<>
|
||||||
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
|
<ContextMenu.Item
|
||||||
|
Icon={PencilIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||||
|
else editIssue();
|
||||||
|
}}
|
||||||
|
>
|
||||||
Edit issue
|
Edit issue
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
{!isDraftIssues && (
|
||||||
Make a copy...
|
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
||||||
</ContextMenu.Item>
|
Make a copy...
|
||||||
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
|
</ContextMenu.Item>
|
||||||
|
)}
|
||||||
|
<ContextMenu.Item
|
||||||
|
Icon={TrashIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
|
||||||
|
else handleDeleteIssue(issue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Delete issue
|
Delete issue
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
{!isDraftIssues && (
|
||||||
Copy issue link
|
<>
|
||||||
</ContextMenu.Item>
|
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||||
<a href={issuePath} target="_blank" rel="noreferrer noopener">
|
Copy issue link
|
||||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
</ContextMenu.Item>
|
||||||
Open issue in new tab
|
<a href={issuePath} target="_blank" rel="noreferrer noopener">
|
||||||
</ContextMenu.Item>
|
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||||
</a>
|
Open issue in new tab
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
|
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
@ -247,7 +273,10 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="truncate text-[0.825rem] text-custom-text-100"
|
className="truncate text-[0.825rem] text-custom-text-100"
|
||||||
onClick={() => openPeekOverview(issue)}
|
onClick={() => {
|
||||||
|
if (!isDraftIssues) openPeekOverview(issue);
|
||||||
|
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{issue.name}
|
{issue.name}
|
||||||
</button>
|
</button>
|
||||||
@ -345,7 +374,12 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{type && !isNotAllowed && (
|
{type && !isNotAllowed && (
|
||||||
<CustomMenu width="auto" ellipsis>
|
<CustomMenu width="auto" ellipsis>
|
||||||
<CustomMenu.MenuItem onClick={editIssue}>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||||
|
else editIssue();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<PencilIcon className="h-4 w-4" />
|
<PencilIcon className="h-4 w-4" />
|
||||||
<span>Edit issue</span>
|
<span>Edit issue</span>
|
||||||
@ -359,18 +393,25 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
|
||||||
|
else handleDeleteIssue(issue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<TrashIcon className="h-4 w-4" />
|
<TrashIcon className="h-4 w-4" />
|
||||||
<span>Delete issue</span>
|
<span>Delete issue</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
{!isDraftIssues && (
|
||||||
<div className="flex items-center justify-start gap-2">
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
<LinkIcon className="h-4 w-4" />
|
<div className="flex items-center justify-start gap-2">
|
||||||
<span>Copy issue link</span>
|
<LinkIcon className="h-4 w-4" />
|
||||||
</div>
|
<span>Copy issue link</span>
|
||||||
</CustomMenu.MenuItem>
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,6 +40,7 @@ type Props = {
|
|||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
addIssueToGroup: () => void;
|
addIssueToGroup: () => void;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
@ -56,6 +57,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
addIssueToGroup,
|
addIssueToGroup,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
|
handleDraftIssueAction,
|
||||||
handleMyIssueOpen,
|
handleMyIssueOpen,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
@ -253,6 +255,16 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
editIssue={() => handleIssueAction(issue, "edit")}
|
editIssue={() => handleIssueAction(issue, "edit")}
|
||||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
|
handleDraftIssueSelect={
|
||||||
|
handleDraftIssueAction
|
||||||
|
? () => handleDraftIssueAction(issue, "edit")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
handleDraftIssueDelete={
|
||||||
|
handleDraftIssueAction
|
||||||
|
? () => handleDraftIssueAction(issue, "delete")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
handleMyIssueOpen={handleMyIssueOpen}
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
if (removeIssue !== null && issue.bridge_id)
|
if (removeIssue !== null && issue.bridge_id)
|
||||||
|
@ -46,32 +46,38 @@ const IntegrationGuide = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-full space-y-2">
|
<div className="h-full w-full">
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div>
|
||||||
{EXPORTERS_LIST.map((service) => (
|
{EXPORTERS_LIST.map((service) => (
|
||||||
<div
|
<div
|
||||||
key={service.provider}
|
key={service.provider}
|
||||||
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"
|
className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 whitespace-nowrap">
|
<div className="flex items-start justify-between gap-4 w-full">
|
||||||
<div className="relative h-10 w-10 flex-shrink-0">
|
<div className="flex item-center gap-2.5">
|
||||||
<Image
|
<div className="relative h-10 w-10 flex-shrink-0">
|
||||||
src={service.logo}
|
<Image
|
||||||
layout="fill"
|
src={service.logo}
|
||||||
objectFit="cover"
|
layout="fill"
|
||||||
alt={`${service.title} Logo`}
|
objectFit="cover"
|
||||||
/>
|
alt={`${service.title} Logo`}
|
||||||
</div>
|
/>
|
||||||
<div className="w-full">
|
</div>
|
||||||
<h3>{service.title}</h3>
|
<div>
|
||||||
<p className="text-sm text-custom-text-200">{service.description}</p>
|
<h3 className="flex items-center gap-4 text-sm font-medium">
|
||||||
|
{service.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}>
|
<Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}>
|
||||||
<a>
|
<a>
|
||||||
<PrimaryButton>
|
<PrimaryButton>
|
||||||
<span className="capitalize">{service.type}</span> now
|
<span className="capitalize">{service.type}</span>
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -80,10 +86,11 @@ const IntegrationGuide = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4">
|
<div>
|
||||||
<h3 className="mb-2 flex gap-2 text-lg font-medium justify-between">
|
<div className="flex items-center justify-between pt-7 pb-3.5 border-b border-custom-border-200">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="">Previous Exports</div>
|
<h3 className="flex gap-2 text-xl font-medium">Previous Exports</h3>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
|
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
|
||||||
@ -128,27 +135,29 @@ const IntegrationGuide = () => {
|
|||||||
<Icon iconName="keyboard_arrow_right" className="!text-lg" />
|
<Icon iconName="keyboard_arrow_right" className="!text-lg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</div>
|
||||||
{exporterServices && exporterServices?.results ? (
|
<div className="flex flex-col">
|
||||||
exporterServices?.results?.length > 0 ? (
|
{exporterServices && exporterServices?.results ? (
|
||||||
<div className="space-y-2">
|
exporterServices?.results?.length > 0 ? (
|
||||||
<div className="divide-y divide-custom-border-200">
|
<div>
|
||||||
{exporterServices?.results.map((service) => (
|
<div className="divide-y divide-custom-border-200">
|
||||||
<SingleExport key={service.id} service={service} refreshing={refreshing} />
|
{exporterServices?.results.map((service) => (
|
||||||
))}
|
<SingleExport key={service.id} service={service} refreshing={refreshing} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<p className="py-2 text-sm text-custom-text-200">No previous export available.</p>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<p className="py-2 text-sm text-custom-text-200">No previous export available.</p>
|
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||||
)
|
<Loader.Item height="40px" width="100%" />
|
||||||
) : (
|
<Loader.Item height="40px" width="100%" />
|
||||||
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
<Loader.Item height="40px" width="100%" />
|
||||||
<Loader.Item height="40px" width="100%" />
|
<Loader.Item height="40px" width="100%" />
|
||||||
<Loader.Item height="40px" width="100%" />
|
</Loader>
|
||||||
<Loader.Item height="40px" width="100%" />
|
)}
|
||||||
<Loader.Item height="40px" width="100%" />
|
</div>
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
{provider && (
|
{provider && (
|
||||||
|
@ -23,7 +23,7 @@ export const SingleExport: React.FC<Props> = ({ service, refreshing }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-2 py-3">
|
<div className="flex items-center justify-between gap-2 px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="flex items-center gap-2 text-sm">
|
<h4 className="flex items-center gap-2 text-sm">
|
||||||
<span>
|
<span>
|
||||||
|
@ -21,7 +21,6 @@ import {
|
|||||||
import { Loader, PrimaryButton } from "components/ui";
|
import { Loader, PrimaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||||
import { ArrowRightIcon } from "components/icons";
|
|
||||||
// types
|
// types
|
||||||
import { IImporterService } from "types";
|
import { IImporterService } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -57,10 +56,10 @@ const IntegrationGuide = () => {
|
|||||||
data={importToDelete}
|
data={importToDelete}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<div className="h-full space-y-2">
|
<div className="h-full">
|
||||||
{(!provider || provider === "csv") && (
|
{(!provider || provider === "csv") && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-5 flex items-center gap-2">
|
{/* <div className="mb-5 flex items-center gap-2">
|
||||||
<div className="h-full w-full space-y-1">
|
<div className="h-full w-full space-y-1">
|
||||||
<div className="text-lg font-medium">Relocation Guide</div>
|
<div className="text-lg font-medium">Relocation Guide</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
@ -78,85 +77,87 @@ const IntegrationGuide = () => {
|
|||||||
<ArrowRightIcon width={"18px"} color={"#3F76FF"} />
|
<ArrowRightIcon width={"18px"} color={"#3F76FF"} />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div> */}
|
||||||
<div className="space-y-2">
|
{IMPORTERS_EXPORTERS_LIST.map((service) => (
|
||||||
{IMPORTERS_EXPORTERS_LIST.map((service) => (
|
<div
|
||||||
<div
|
key={service.provider}
|
||||||
key={service.provider}
|
className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6"
|
||||||
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"
|
>
|
||||||
>
|
<div className="flex items-start gap-4">
|
||||||
<div className="flex items-center gap-4 whitespace-nowrap">
|
<div className="relative h-10 w-10 flex-shrink-0">
|
||||||
<div className="relative h-10 w-10 flex-shrink-0">
|
<Image
|
||||||
<Image
|
src={service.logo}
|
||||||
src={service.logo}
|
layout="fill"
|
||||||
layout="fill"
|
objectFit="cover"
|
||||||
objectFit="cover"
|
alt={`${service.title} Logo`}
|
||||||
alt={`${service.title} Logo`}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div className="w-full">
|
<h3 className="flex items-center gap-4 text-sm font-medium">{service.title}</h3>
|
||||||
<h3>{service.title}</h3>
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
<p className="text-sm text-custom-text-200">{service.description}</p>
|
{service.description}
|
||||||
</div>
|
</p>
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<Link
|
|
||||||
href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}
|
|
||||||
>
|
|
||||||
<a>
|
|
||||||
<PrimaryButton>
|
|
||||||
<span className="capitalize">{service.type}</span> now
|
|
||||||
</PrimaryButton>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="flex-shrink-0">
|
||||||
</div>
|
<Link href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}>
|
||||||
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4">
|
<a>
|
||||||
<h3 className="mb-2 flex gap-2 text-lg font-medium">
|
<PrimaryButton>
|
||||||
Previous Imports
|
<span className="capitalize">{service.type}</span>
|
||||||
<button
|
</PrimaryButton>
|
||||||
type="button"
|
</a>
|
||||||
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
|
</Link>
|
||||||
onClick={() => {
|
</div>
|
||||||
setRefreshing(true);
|
</div>
|
||||||
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
|
))}
|
||||||
setRefreshing(false)
|
<div>
|
||||||
);
|
<div className="flex items-center pt-7 pb-3.5 border-b border-custom-border-200">
|
||||||
}}
|
<h3 className="flex gap-2 text-xl font-medium">
|
||||||
>
|
Previous Imports
|
||||||
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
|
<button
|
||||||
{refreshing ? "Refreshing..." : "Refresh status"}
|
type="button"
|
||||||
</button>
|
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
|
||||||
</h3>
|
onClick={() => {
|
||||||
{importerServices ? (
|
setRefreshing(true);
|
||||||
importerServices.length > 0 ? (
|
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
|
||||||
<div className="space-y-2">
|
setRefreshing(false)
|
||||||
<div className="divide-y divide-custom-border-200">
|
);
|
||||||
{importerServices.map((service) => (
|
}}
|
||||||
<SingleImport
|
>
|
||||||
key={service.id}
|
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
|
||||||
service={service}
|
{refreshing ? "Refreshing..." : "Refresh status"}
|
||||||
refreshing={refreshing}
|
</button>
|
||||||
handleDelete={() => handleDeleteImport(service)}
|
</h3>
|
||||||
/>
|
</div>
|
||||||
))}
|
<div className="flex flex-col px-4 py-6">
|
||||||
|
{importerServices ? (
|
||||||
|
importerServices.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="divide-y divide-custom-border-200">
|
||||||
|
{importerServices.map((service) => (
|
||||||
|
<SingleImport
|
||||||
|
key={service.id}
|
||||||
|
service={service}
|
||||||
|
refreshing={refreshing}
|
||||||
|
handleDelete={() => handleDeleteImport(service)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<p className="py-2 text-sm text-custom-text-200">
|
||||||
|
No previous imports available.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<p className="py-2 text-sm text-custom-text-200">
|
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||||
No previous imports available.
|
<Loader.Item height="40px" width="100%" />
|
||||||
</p>
|
<Loader.Item height="40px" width="100%" />
|
||||||
)
|
<Loader.Item height="40px" width="100%" />
|
||||||
) : (
|
<Loader.Item height="40px" width="100%" />
|
||||||
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
</Loader>
|
||||||
<Loader.Item height="40px" width="100%" />
|
)}
|
||||||
<Loader.Item height="40px" width="100%" />
|
</div>
|
||||||
<Loader.Item height="40px" width="100%" />
|
|
||||||
<Loader.Item height="40px" width="100%" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -16,7 +16,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => (
|
export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => (
|
||||||
<div className="flex items-center justify-between gap-2 py-3">
|
<div className="flex items-center justify-between gap-2 px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="flex items-center gap-2 text-sm">
|
<h4 className="flex items-center gap-2 text-sm">
|
||||||
<span>
|
<span>
|
||||||
|
@ -15,6 +15,7 @@ import { DangerButton, Loader, PrimaryButton } from "components/ui";
|
|||||||
// icons
|
// icons
|
||||||
import GithubLogo from "public/services/github.png";
|
import GithubLogo from "public/services/github.png";
|
||||||
import SlackLogo from "public/services/slack.png";
|
import SlackLogo from "public/services/slack.png";
|
||||||
|
import { CheckCircle2 } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IAppIntegration, IWorkspaceIntegration } from "types";
|
import { IAppIntegration, IWorkspaceIntegration } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -27,13 +28,12 @@ type Props = {
|
|||||||
const integrationDetails: { [key: string]: any } = {
|
const integrationDetails: { [key: string]: any } = {
|
||||||
github: {
|
github: {
|
||||||
logo: GithubLogo,
|
logo: GithubLogo,
|
||||||
installed:
|
installed: "Activate GitHub on individual projects to sync with specific repositories.",
|
||||||
"Activate GitHub integrations on individual projects to sync with specific repositories.",
|
|
||||||
notInstalled: "Connect with GitHub with your Plane workspace to sync project issues.",
|
notInstalled: "Connect with GitHub with your Plane workspace to sync project issues.",
|
||||||
},
|
},
|
||||||
slack: {
|
slack: {
|
||||||
logo: SlackLogo,
|
logo: SlackLogo,
|
||||||
installed: "Activate Slack integrations on individual projects to sync with specific channels.",
|
installed: "Activate Slack on individual projects to sync with specific channels.",
|
||||||
notInstalled: "Connect with Slack with your Plane workspace to sync project issues.",
|
notInstalled: "Connect with Slack with your Plane workspace to sync project issues.",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -99,31 +99,22 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5">
|
<div className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="h-12 w-12 flex-shrink-0">
|
<div className="h-10 w-10 flex-shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={integrationDetails[integration.provider].logo}
|
src={integrationDetails[integration.provider].logo}
|
||||||
alt={`${integration.title} Logo`}
|
alt={`${integration.title} Logo`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="flex items-center gap-4 text-xl font-semibold">
|
<h3 className="flex items-center gap-2 text-sm font-medium">
|
||||||
{integration.title}
|
{integration.title}
|
||||||
{workspaceIntegrations ? (
|
{workspaceIntegrations
|
||||||
isInstalled ? (
|
? isInstalled && <CheckCircle2 className="h-3.5 w-3.5 text-white fill-green-500" />
|
||||||
<span className="flex items-center gap-1 text-sm font-normal text-green-500">
|
: null}
|
||||||
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500" /> Installed
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="flex items-center gap-1 text-sm font-normal text-custom-text-200">
|
|
||||||
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-custom-background-80" />{" "}
|
|
||||||
Not Installed
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
{workspaceIntegrations
|
{workspaceIntegrations
|
||||||
? isInstalled
|
? isInstalled
|
||||||
? integrationDetails[integration.provider].installed
|
? integrationDetails[integration.provider].installed
|
||||||
@ -135,12 +126,12 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
|
|||||||
|
|
||||||
{workspaceIntegrations ? (
|
{workspaceIntegrations ? (
|
||||||
isInstalled ? (
|
isInstalled ? (
|
||||||
<DangerButton onClick={handleRemoveIntegration} loading={deletingIntegration}>
|
<DangerButton onClick={handleRemoveIntegration} loading={deletingIntegration} outline>
|
||||||
{deletingIntegration ? "Removing..." : "Remove installation"}
|
{deletingIntegration ? "Uninstalling..." : "Uninstall"}
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
) : (
|
) : (
|
||||||
<PrimaryButton onClick={startAuth} loading={isInstalling}>
|
<PrimaryButton onClick={startAuth} loading={isInstalling}>
|
||||||
{isInstalling ? "Installing..." : "Add installation"}
|
{isInstalling ? "Installing..." : "Install"}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
93
web/components/issues/confirm-issue-discard.tsx
Normal file
93
web/components/issues/confirm-issue-discard.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { SecondaryButton, PrimaryButton } from "components/ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
onDiscard: () => void;
|
||||||
|
onConfirm: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
|
||||||
|
const { isOpen, handleClose, onDiscard, onConfirm } = props;
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletion = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await onConfirm();
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
|
||||||
|
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div className="sm:flex sm:items-start">
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||||
|
>
|
||||||
|
Draft Issue
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-custom-text-200">
|
||||||
|
Would you like to save this issue in drafts?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between gap-2 p-4 sm:px-6">
|
||||||
|
<div>
|
||||||
|
<SecondaryButton onClick={onDiscard}>Discard</SecondaryButton>
|
||||||
|
</div>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||||
|
<PrimaryButton onClick={handleDeletion} loading={isLoading}>
|
||||||
|
{isLoading ? "Saving..." : "Save Draft"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
145
web/components/issues/delete-draft-issue-modal.tsx
Normal file
145
web/components/issues/delete-draft-issue-modal.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import issueServices from "services/issues.service";
|
||||||
|
// hooks
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// icons
|
||||||
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
|
// ui
|
||||||
|
import { SecondaryButton, DangerButton } from "components/ui";
|
||||||
|
// types
|
||||||
|
import type { IIssue, ICurrentUserResponse } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
data: IIssue | null;
|
||||||
|
user?: ICurrentUserResponse;
|
||||||
|
onSubmit?: () => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
|
||||||
|
const { isOpen, handleClose, data, user, onSubmit } = props;
|
||||||
|
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { params } = useIssuesView();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletion = async () => {
|
||||||
|
if (!workspaceSlug || !data) return;
|
||||||
|
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
await issueServices
|
||||||
|
.deleteDraftIssue(workspaceSlug as string, data.project, data.id)
|
||||||
|
.then(() => {
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
handleClose();
|
||||||
|
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
|
||||||
|
setToastAlert({
|
||||||
|
title: "Success",
|
||||||
|
message: "Draft Issue deleted successfully",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
handleClose();
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error",
|
||||||
|
message: "Something went wrong",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
});
|
||||||
|
if (onSubmit) await onSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||||
|
<div className="flex flex-col gap-6 p-6">
|
||||||
|
<div className="flex w-full items-center justify-start gap-6">
|
||||||
|
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||||
|
<ExclamationTriangleIcon
|
||||||
|
className="h-6 w-6 text-red-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center justify-start">
|
||||||
|
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Draft Issue</h3>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
<p className="text-sm text-custom-text-200">
|
||||||
|
Are you sure you want to delete issue{" "}
|
||||||
|
<span className="break-words font-medium text-custom-text-100">
|
||||||
|
{data?.project_detail.identifier}-{data?.sequence_id}
|
||||||
|
</span>
|
||||||
|
{""}? All of the data related to the draft issue will be permanently removed.
|
||||||
|
This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||||
|
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
||||||
|
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
|
||||||
|
</DangerButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
580
web/components/issues/draft-issue-form.tsx
Normal file
580
web/components/issues/draft-issue-form.tsx
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
import React, { FC, useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import aiService from "services/ai.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// components
|
||||||
|
import { GptAssistantModal } from "components/core";
|
||||||
|
import { ParentIssuesListModal } from "components/issues";
|
||||||
|
import {
|
||||||
|
IssueAssigneeSelect,
|
||||||
|
IssueDateSelect,
|
||||||
|
IssueEstimateSelect,
|
||||||
|
IssueLabelSelect,
|
||||||
|
IssuePrioritySelect,
|
||||||
|
IssueProjectSelect,
|
||||||
|
IssueStateSelect,
|
||||||
|
} from "components/issues/select";
|
||||||
|
import { CreateStateModal } from "components/states";
|
||||||
|
import { CreateLabelModal } from "components/labels";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu, Input, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||||
|
import { TipTapEditor } from "components/tiptap";
|
||||||
|
// icons
|
||||||
|
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
|
||||||
|
|
||||||
|
const defaultValues: Partial<IIssue> = {
|
||||||
|
project: "",
|
||||||
|
name: "",
|
||||||
|
description: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description_html: "<p></p>",
|
||||||
|
estimate_point: null,
|
||||||
|
state: "",
|
||||||
|
parent: null,
|
||||||
|
priority: "none",
|
||||||
|
assignees: [],
|
||||||
|
assignees_list: [],
|
||||||
|
labels: [],
|
||||||
|
labels_list: [],
|
||||||
|
start_date: null,
|
||||||
|
target_date: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IssueFormProps {
|
||||||
|
handleFormSubmit: (formData: Partial<IIssue>) => Promise<void>;
|
||||||
|
data?: Partial<IIssue> | null;
|
||||||
|
prePopulatedData?: Partial<IIssue> | null;
|
||||||
|
projectId: string;
|
||||||
|
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
createMore: boolean;
|
||||||
|
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
handleClose: () => void;
|
||||||
|
status: boolean;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
fieldsToShow: (
|
||||||
|
| "project"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "state"
|
||||||
|
| "priority"
|
||||||
|
| "assignee"
|
||||||
|
| "label"
|
||||||
|
| "startDate"
|
||||||
|
| "dueDate"
|
||||||
|
| "estimate"
|
||||||
|
| "parent"
|
||||||
|
| "all"
|
||||||
|
)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||||
|
const {
|
||||||
|
handleFormSubmit,
|
||||||
|
data,
|
||||||
|
prePopulatedData,
|
||||||
|
projectId,
|
||||||
|
setActiveProject,
|
||||||
|
createMore,
|
||||||
|
setCreateMore,
|
||||||
|
handleClose,
|
||||||
|
status,
|
||||||
|
user,
|
||||||
|
fieldsToShow,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [stateModal, setStateModal] = useState(false);
|
||||||
|
const [labelModal, setLabelModal] = useState(false);
|
||||||
|
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||||
|
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||||
|
|
||||||
|
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||||
|
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||||
|
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
control,
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
|
setFocus,
|
||||||
|
} = useForm<IIssue>({
|
||||||
|
defaultValues: prePopulatedData ?? defaultValues,
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const issueName = watch("name");
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateUpdateIssue = async (
|
||||||
|
formData: Partial<IIssue>,
|
||||||
|
action: "saveDraft" | "createToNewIssue" = "saveDraft"
|
||||||
|
) => {
|
||||||
|
await handleFormSubmit({
|
||||||
|
...formData,
|
||||||
|
is_draft: action === "saveDraft",
|
||||||
|
});
|
||||||
|
|
||||||
|
setGptAssistantModal(false);
|
||||||
|
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
project: projectId,
|
||||||
|
description: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description_html: "<p></p>",
|
||||||
|
});
|
||||||
|
editorRef?.current?.clearEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAiAssistance = async (response: string) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
setValue("description", {});
|
||||||
|
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
|
||||||
|
editorRef.current?.setEditorValue(`${watch("description_html")}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoGenerateDescription = async () => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
setIAmFeelingLucky(true);
|
||||||
|
|
||||||
|
aiService
|
||||||
|
.createGptTask(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
{
|
||||||
|
prompt: issueName,
|
||||||
|
task: "Generate a proper description for this issue.",
|
||||||
|
},
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.response === "")
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message:
|
||||||
|
"Issue title isn't informative enough to generate the description. Please try with a different title.",
|
||||||
|
});
|
||||||
|
else handleAiAssistance(res.response_html);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const error = err?.data?.error;
|
||||||
|
|
||||||
|
if (err.status === 429)
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message:
|
||||||
|
error ||
|
||||||
|
"You have reached the maximum number of requests of 50 requests per month per user.",
|
||||||
|
});
|
||||||
|
else
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: error || "Some error occurred. Please try again.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => setIAmFeelingLucky(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus("name");
|
||||||
|
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
...(prePopulatedData ?? {}),
|
||||||
|
...(data ?? {}),
|
||||||
|
});
|
||||||
|
}, [setFocus, prePopulatedData, reset, data]);
|
||||||
|
|
||||||
|
// update projectId in form when projectId changes
|
||||||
|
useEffect(() => {
|
||||||
|
reset({
|
||||||
|
...getValues(),
|
||||||
|
project: projectId,
|
||||||
|
});
|
||||||
|
}, [getValues, projectId, reset]);
|
||||||
|
|
||||||
|
const startDate = watch("start_date");
|
||||||
|
const targetDate = watch("target_date");
|
||||||
|
|
||||||
|
const minDate = startDate ? new Date(startDate) : null;
|
||||||
|
minDate?.setDate(minDate.getDate());
|
||||||
|
|
||||||
|
const maxDate = targetDate ? new Date(targetDate) : null;
|
||||||
|
maxDate?.setDate(maxDate.getDate());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{projectId && (
|
||||||
|
<>
|
||||||
|
<CreateStateModal
|
||||||
|
isOpen={stateModal}
|
||||||
|
handleClose={() => setStateModal(false)}
|
||||||
|
projectId={projectId}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
<CreateLabelModal
|
||||||
|
isOpen={labelModal}
|
||||||
|
handleClose={() => setLabelModal(false)}
|
||||||
|
projectId={projectId}
|
||||||
|
user={user}
|
||||||
|
onSuccess={(response) => {
|
||||||
|
setValue("labels", [...watch("labels"), response.id]);
|
||||||
|
setValue("labels_list", [...watch("labels_list"), response.id]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((formData) => handleCreateUpdateIssue(formData, "createToNewIssue"))}
|
||||||
|
>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="project"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueProjectSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
onChange(val);
|
||||||
|
setActiveProject(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h3 className="text-xl font-semibold leading-6 text-custom-text-100">
|
||||||
|
{status ? "Update" : "Create"} Issue
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{watch("parent") &&
|
||||||
|
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
|
||||||
|
selectedParentIssue && (
|
||||||
|
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="block h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: selectedParentIssue.state__color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="flex-shrink-0 text-custom-text-200">
|
||||||
|
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
|
||||||
|
</span>
|
||||||
|
<span className="truncate font-medium">
|
||||||
|
{selectedParentIssue.name.substring(0, 50)}
|
||||||
|
</span>
|
||||||
|
<XMarkIcon
|
||||||
|
className="h-3 w-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setValue("parent", null);
|
||||||
|
setSelectedParentIssue(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="mt-2 space-y-3">
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("name")) && (
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
className="resize-none text-xl"
|
||||||
|
placeholder="Title"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.name}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Title is required",
|
||||||
|
maxLength: {
|
||||||
|
value: 255,
|
||||||
|
message: "Title should be less than 255 characters",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{issueName && issueName !== "" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
|
||||||
|
iAmFeelingLucky ? "cursor-wait" : ""
|
||||||
|
}`}
|
||||||
|
onClick={handleAutoGenerateDescription}
|
||||||
|
disabled={iAmFeelingLucky}
|
||||||
|
>
|
||||||
|
{iAmFeelingLucky ? (
|
||||||
|
"Generating response..."
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
||||||
|
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
<SparklesIcon className="h-4 w-4" />
|
||||||
|
AI
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="description_html"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => {
|
||||||
|
if (!value && !watch("description_html")) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TipTapEditor
|
||||||
|
workspaceSlug={workspaceSlug as string}
|
||||||
|
ref={editorRef}
|
||||||
|
debouncedUpdatesEnabled={false}
|
||||||
|
value={
|
||||||
|
!value ||
|
||||||
|
value === "" ||
|
||||||
|
(typeof value === "object" && Object.keys(value).length === 0)
|
||||||
|
? watch("description_html")
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
customClassName="min-h-[150px]"
|
||||||
|
onChange={(description: Object, description_html: string) => {
|
||||||
|
onChange(description_html);
|
||||||
|
setValue("description", description);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<GptAssistantModal
|
||||||
|
isOpen={gptAssistantModal}
|
||||||
|
handleClose={() => {
|
||||||
|
setGptAssistantModal(false);
|
||||||
|
// this is done so that the title do not reset after gpt popover closed
|
||||||
|
reset(getValues());
|
||||||
|
}}
|
||||||
|
inset="top-2 left-0"
|
||||||
|
content=""
|
||||||
|
htmlContent={watch("description_html")}
|
||||||
|
onResponse={(response) => {
|
||||||
|
handleAiAssistance(response);
|
||||||
|
}}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="state"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueStateSelect
|
||||||
|
setIsOpen={setStateModal}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="priority"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssuePrioritySelect value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="assignees"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueAssigneeSelect
|
||||||
|
projectId={projectId}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="labels"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueLabelSelect
|
||||||
|
setIsOpen={setLabelModal}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="start_date"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueDateSelect
|
||||||
|
label="Start date"
|
||||||
|
maxDate={maxDate ?? undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="target_date"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueDateSelect
|
||||||
|
label="Due date"
|
||||||
|
minDate={minDate ?? undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="estimate_point"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueEstimateSelect value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="parent"
|
||||||
|
render={({ field: { onChange } }) => (
|
||||||
|
<ParentIssuesListModal
|
||||||
|
isOpen={parentIssueListModalOpen}
|
||||||
|
handleClose={() => setParentIssueListModalOpen(false)}
|
||||||
|
onChange={(issue) => {
|
||||||
|
onChange(issue.id);
|
||||||
|
setSelectedParentIssue(issue);
|
||||||
|
}}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||||
|
<CustomMenu ellipsis>
|
||||||
|
{watch("parent") ? (
|
||||||
|
<>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
onClick={() => setParentIssueListModalOpen(true)}
|
||||||
|
>
|
||||||
|
Change parent issue
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
onClick={() => setValue("parent", null)}
|
||||||
|
>
|
||||||
|
Remove parent issue
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
onClick={() => setParentIssueListModalOpen(true)}
|
||||||
|
>
|
||||||
|
Select Parent Issue
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-200 px-5 pt-5">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center gap-1"
|
||||||
|
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
<span className="text-xs">Create more</span>
|
||||||
|
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton onClick={onClose}>Discard</SecondaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={handleSubmit((formData) => handleCreateUpdateIssue(formData, "saveDraft"))}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Saving..." : "Save Draft"}
|
||||||
|
</SecondaryButton>
|
||||||
|
{data && (
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Add Issue"}
|
||||||
|
</PrimaryButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
285
web/components/issues/draft-issue-modal.tsx
Normal file
285
web/components/issues/draft-issue-modal.tsx
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
|
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||||
|
// components
|
||||||
|
import { DraftIssueForm } from "components/issues";
|
||||||
|
// types
|
||||||
|
import type { IIssue } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import {
|
||||||
|
PROJECT_ISSUES_DETAILS,
|
||||||
|
USER_ISSUE,
|
||||||
|
SUB_ISSUES,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
VIEW_ISSUES,
|
||||||
|
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
|
interface IssuesModalProps {
|
||||||
|
data?: IIssue | null;
|
||||||
|
handleClose: () => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
isUpdatingSingleIssue?: boolean;
|
||||||
|
prePopulateData?: Partial<IIssue>;
|
||||||
|
fieldsToShow?: (
|
||||||
|
| "project"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "state"
|
||||||
|
| "priority"
|
||||||
|
| "assignee"
|
||||||
|
| "label"
|
||||||
|
| "startDate"
|
||||||
|
| "dueDate"
|
||||||
|
| "estimate"
|
||||||
|
| "parent"
|
||||||
|
| "all"
|
||||||
|
)[];
|
||||||
|
onSubmit?: (data: Partial<IIssue>) => Promise<void> | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||||
|
data,
|
||||||
|
handleClose,
|
||||||
|
isOpen,
|
||||||
|
isUpdatingSingleIssue = false,
|
||||||
|
prePopulateData,
|
||||||
|
fieldsToShow = ["all"],
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
// states
|
||||||
|
const [createMore, setCreateMore] = useState(false);
|
||||||
|
const [activeProject, setActiveProject] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
|
const { displayFilters, params } = useIssuesView();
|
||||||
|
const { params: calendarParams } = useCalendarIssuesView();
|
||||||
|
const { ...viewGanttParams } = params;
|
||||||
|
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
const { projects } = useProjects();
|
||||||
|
|
||||||
|
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
||||||
|
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
|
||||||
|
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
|
||||||
|
prePopulateData = {
|
||||||
|
...prePopulateData,
|
||||||
|
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
setActiveProject(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if modal is closed, reset active project to null
|
||||||
|
// and return to avoid activeProject being set to some other project
|
||||||
|
if (!isOpen) {
|
||||||
|
setActiveProject(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if data is present, set active project to the project of the
|
||||||
|
// issue. This has more priority than the project in the url.
|
||||||
|
if (data && data.project) {
|
||||||
|
setActiveProject(data.project);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if data is not present, set active project to the project
|
||||||
|
// in the url. This has the least priority.
|
||||||
|
if (projects && projects.length > 0 && !activeProject)
|
||||||
|
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||||
|
}, [activeProject, data, projectId, projects, isOpen]);
|
||||||
|
|
||||||
|
const calendarFetchKey = cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
|
||||||
|
: moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
|
||||||
|
: viewId
|
||||||
|
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams);
|
||||||
|
|
||||||
|
const spreadsheetFetchKey = cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
|
||||||
|
: moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
|
||||||
|
: viewId
|
||||||
|
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", spreadsheetParams);
|
||||||
|
|
||||||
|
const ganttFetchKey = cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
|
||||||
|
: moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString())
|
||||||
|
: viewId
|
||||||
|
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "");
|
||||||
|
|
||||||
|
const createIssue = async (payload: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug || !activeProject || !user) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
|
||||||
|
.then(async () => {
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
|
||||||
|
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||||
|
if (displayFilters.layout === "gantt_chart")
|
||||||
|
mutate(ganttFetchKey, {
|
||||||
|
start_target_date: true,
|
||||||
|
order_by: "sort_order",
|
||||||
|
});
|
||||||
|
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||||
|
if (groupedIssues) mutateMyIssues();
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success!",
|
||||||
|
message: "Issue created successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
|
||||||
|
mutate(USER_ISSUE(workspaceSlug as string));
|
||||||
|
|
||||||
|
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Issue could not be created. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createMore) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateIssue = async (payload: Partial<IIssue>) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.updateDraftIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
|
||||||
|
.then((res) => {
|
||||||
|
if (isUpdatingSingleIssue) {
|
||||||
|
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
||||||
|
} else {
|
||||||
|
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||||
|
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||||
|
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createMore) onClose();
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success!",
|
||||||
|
message: "Issue updated successfully.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Issue could not be updated. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (formData: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug || !activeProject) return;
|
||||||
|
|
||||||
|
const payload: Partial<IIssue> = {
|
||||||
|
...formData,
|
||||||
|
assignees_list: formData.assignees ?? [],
|
||||||
|
labels_list: formData.labels ?? [],
|
||||||
|
description: formData.description ?? "",
|
||||||
|
description_html: formData.description_html ?? "<p></p>",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data) await createIssue(payload);
|
||||||
|
else await updateIssue(payload);
|
||||||
|
|
||||||
|
if (onSubmit) await onSubmit(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!projects || projects.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||||
|
<DraftIssueForm
|
||||||
|
handleFormSubmit={handleFormSubmit}
|
||||||
|
prePopulatedData={prePopulateData}
|
||||||
|
data={data}
|
||||||
|
createMore={createMore}
|
||||||
|
setCreateMore={setCreateMore}
|
||||||
|
handleClose={onClose}
|
||||||
|
projectId={activeProject ?? ""}
|
||||||
|
setActiveProject={setActiveProject}
|
||||||
|
status={data ? true : false}
|
||||||
|
user={user}
|
||||||
|
fieldsToShow={fieldsToShow}
|
||||||
|
/>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -11,6 +11,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import aiService from "services/ai.service";
|
import aiService from "services/ai.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import { GptAssistantModal } from "components/core";
|
import { GptAssistantModal } from "components/core";
|
||||||
import { ParentIssuesListModal } from "components/issues";
|
import { ParentIssuesListModal } from "components/issues";
|
||||||
@ -59,8 +60,11 @@ export interface IssueFormProps {
|
|||||||
createMore: boolean;
|
createMore: boolean;
|
||||||
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
|
handleDiscardClose: () => void;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
|
setIsConfirmDiscardOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
handleFormDirty: (payload: Partial<IIssue> | null) => void;
|
||||||
fieldsToShow: (
|
fieldsToShow: (
|
||||||
| "project"
|
| "project"
|
||||||
| "name"
|
| "name"
|
||||||
@ -78,7 +82,11 @@ export interface IssueFormProps {
|
|||||||
)[];
|
)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||||
|
=======
|
||||||
|
export const IssueForm: FC<IssueFormProps> = (props) => {
|
||||||
|
>>>>>>> 3d72279edbe0889626647b1aecf29c86a33746f4
|
||||||
const {
|
const {
|
||||||
handleFormSubmit,
|
handleFormSubmit,
|
||||||
initialData,
|
initialData,
|
||||||
@ -86,10 +94,18 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
setActiveProject,
|
setActiveProject,
|
||||||
createMore,
|
createMore,
|
||||||
setCreateMore,
|
setCreateMore,
|
||||||
|
<<<<<<< HEAD
|
||||||
handleClose,
|
handleClose,
|
||||||
status,
|
status,
|
||||||
user,
|
user,
|
||||||
fieldsToShow,
|
fieldsToShow,
|
||||||
|
=======
|
||||||
|
handleDiscardClose,
|
||||||
|
status,
|
||||||
|
user,
|
||||||
|
fieldsToShow,
|
||||||
|
handleFormDirty,
|
||||||
|
>>>>>>> 3d72279edbe0889626647b1aecf29c86a33746f4
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [stateModal, setStateModal] = useState(false);
|
const [stateModal, setStateModal] = useState(false);
|
||||||
@ -100,7 +116,11 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
const [attributesList, setAttributesList] = useState<{ [key: string]: string[] }>({});
|
const [attributesList, setAttributesList] = useState<{ [key: string]: string[] }>({});
|
||||||
|
=======
|
||||||
|
const { setValue: setValueInLocalStorage } = useLocalStorage<any>("draftedIssue", null);
|
||||||
|
>>>>>>> 3d72279edbe0889626647b1aecf29c86a33746f4
|
||||||
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
@ -114,7 +134,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting, isDirty },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
watch,
|
||||||
@ -129,6 +149,23 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
|
|
||||||
const issueName = watch("name");
|
const issueName = watch("name");
|
||||||
|
|
||||||
|
const payload: Partial<IIssue> = {
|
||||||
|
name: getValues("name"),
|
||||||
|
description: getValues("description"),
|
||||||
|
state: getValues("state"),
|
||||||
|
priority: getValues("priority"),
|
||||||
|
assignees: getValues("assignees"),
|
||||||
|
target_date: getValues("target_date"),
|
||||||
|
labels: getValues("labels"),
|
||||||
|
project: getValues("project"),
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDirty) handleFormDirty(payload);
|
||||||
|
else handleFormDirty(null);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [JSON.stringify(payload), isDirty]);
|
||||||
|
|
||||||
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData);
|
||||||
|
|
||||||
@ -581,7 +618,15 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
|
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
|
<SecondaryButton
|
||||||
|
onClick={() => {
|
||||||
|
const data = JSON.stringify(getValues());
|
||||||
|
setValueInLocalStorage(data);
|
||||||
|
handleDiscardClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</SecondaryButton>
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
{status
|
{status
|
||||||
? isSubmitting
|
? isSubmitting
|
||||||
|
@ -16,3 +16,10 @@ export * from "./sidebar";
|
|||||||
export * from "./sub-issues-list";
|
export * from "./sub-issues-list";
|
||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./issue-reaction";
|
export * from "./issue-reaction";
|
||||||
|
export * from "./peek-overview";
|
||||||
|
export * from "./confirm-issue-discard";
|
||||||
|
|
||||||
|
// draft issue
|
||||||
|
export * from "./draft-issue-form";
|
||||||
|
export * from "./draft-issue-modal";
|
||||||
|
export * from "./delete-draft-issue-modal";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useCallback } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
|||||||
import useProjects from "hooks/use-projects";
|
import useProjects from "hooks/use-projects";
|
||||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||||
// components
|
// components
|
||||||
import { IssueForm } from "components/issues";
|
import { IssueForm, ConfirmIssueDiscard } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -35,6 +35,7 @@ import {
|
|||||||
MODULE_DETAILS,
|
MODULE_DETAILS,
|
||||||
VIEW_ISSUES,
|
VIEW_ISSUES,
|
||||||
INBOX_ISSUES,
|
INBOX_ISSUES,
|
||||||
|
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
||||||
@ -73,6 +74,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
// states
|
// states
|
||||||
const [createMore, setCreateMore] = useState(false);
|
const [createMore, setCreateMore] = useState(false);
|
||||||
|
const [formDirtyState, setFormDirtyState] = useState<any>(null);
|
||||||
|
const [showConfirmDiscard, setShowConfirmDiscard] = useState(false);
|
||||||
const [activeProject, setActiveProject] = useState<string | null>(null);
|
const [activeProject, setActiveProject] = useState<string | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -80,7 +83,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
|
|
||||||
const { displayFilters, params } = useIssuesView();
|
const { displayFilters, params } = useIssuesView();
|
||||||
const { params: calendarParams } = useCalendarIssuesView();
|
const { params: calendarParams } = useCalendarIssuesView();
|
||||||
const { order_by, group_by, ...viewGanttParams } = params;
|
const { ...viewGanttParams } = params;
|
||||||
const { params: inboxParams } = useInboxView();
|
const { params: inboxParams } = useInboxView();
|
||||||
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
@ -99,10 +102,23 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = () => {
|
||||||
|
if (formDirtyState !== null) {
|
||||||
|
setShowConfirmDiscard(true);
|
||||||
|
} else {
|
||||||
|
handleClose();
|
||||||
|
setActiveProject(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDiscardClose = () => {
|
||||||
handleClose();
|
handleClose();
|
||||||
setActiveProject(null);
|
setActiveProject(null);
|
||||||
}, [handleClose]);
|
};
|
||||||
|
|
||||||
|
const handleFormDirty = (data: any) => {
|
||||||
|
setFormDirtyState(data);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// if modal is closed, reset active project to null
|
// if modal is closed, reset active project to null
|
||||||
@ -275,10 +291,50 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createMore) onClose();
|
if (!createMore) onDiscardClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDraftIssue = async () => {
|
||||||
|
if (!workspaceSlug || !activeProject || !user) return;
|
||||||
|
|
||||||
|
const payload: Partial<IIssue> = {
|
||||||
|
...formDirtyState,
|
||||||
|
};
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
|
||||||
|
.then(() => {
|
||||||
|
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
if (groupedIssues) mutateMyIssues();
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success!",
|
||||||
|
message: "Draft Issue created successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
setActiveProject(null);
|
||||||
|
setFormDirtyState(null);
|
||||||
|
setShowConfirmDiscard(false);
|
||||||
|
|
||||||
|
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
|
||||||
|
mutate(USER_ISSUE(workspaceSlug as string));
|
||||||
|
|
||||||
|
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Issue could not be created. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateIssue = async (payload: Partial<IIssue>) => {
|
const updateIssue = async (payload: Partial<IIssue>) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
await issuesService
|
await issuesService
|
||||||
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
|
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -294,7 +350,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
||||||
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
||||||
|
|
||||||
if (!createMore) onClose();
|
if (!createMore) onDiscardClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -331,49 +387,66 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
if (!projects || projects.length === 0) return null;
|
if (!projects || projects.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<>
|
||||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
<ConfirmIssueDiscard
|
||||||
<Transition.Child
|
isOpen={showConfirmDiscard}
|
||||||
as={React.Fragment}
|
handleClose={() => setShowConfirmDiscard(false)}
|
||||||
enter="ease-out duration-300"
|
onConfirm={createDraftIssue}
|
||||||
enterFrom="opacity-0"
|
onDiscard={() => {
|
||||||
enterTo="opacity-100"
|
handleClose();
|
||||||
leave="ease-in duration-200"
|
setActiveProject(null);
|
||||||
leaveFrom="opacity-100"
|
setFormDirtyState(null);
|
||||||
leaveTo="opacity-0"
|
setShowConfirmDiscard(false);
|
||||||
>
|
}}
|
||||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
/>
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
enterFrom="opacity-0"
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
enterTo="opacity-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
<IssueForm
|
</Transition.Child>
|
||||||
handleFormSubmit={handleFormSubmit}
|
|
||||||
initialData={data ?? prePopulateData}
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
createMore={createMore}
|
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
||||||
setCreateMore={setCreateMore}
|
<Transition.Child
|
||||||
handleClose={onClose}
|
as={React.Fragment}
|
||||||
projectId={activeProject ?? ""}
|
enter="ease-out duration-300"
|
||||||
setActiveProject={setActiveProject}
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
status={data ? true : false}
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
user={user}
|
leave="ease-in duration-200"
|
||||||
fieldsToShow={fieldsToShow}
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
/>
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
</Dialog.Panel>
|
>
|
||||||
</Transition.Child>
|
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||||
|
<IssueForm
|
||||||
|
handleFormSubmit={handleFormSubmit}
|
||||||
|
initialData={data ?? prePopulateData}
|
||||||
|
createMore={createMore}
|
||||||
|
setCreateMore={setCreateMore}
|
||||||
|
handleClose={onClose}
|
||||||
|
handleDiscardClose={onDiscardClose}
|
||||||
|
setIsConfirmDiscardOpen={setShowConfirmDiscard}
|
||||||
|
projectId={activeProject ?? ""}
|
||||||
|
setActiveProject={setActiveProject}
|
||||||
|
status={data ? true : false}
|
||||||
|
user={user}
|
||||||
|
fieldsToShow={fieldsToShow}
|
||||||
|
handleFormDirty={handleFormDirty}
|
||||||
|
/>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Dialog>
|
||||||
</Dialog>
|
</Transition.Root>
|
||||||
</Transition.Root>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -205,7 +205,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleIssueAction = useCallback(
|
const handleIssueAction = useCallback(
|
||||||
(issue: IIssue, action: "copy" | "edit" | "delete") => {
|
(issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
|
||||||
if (action === "copy") makeIssueCopy(issue);
|
if (action === "copy") makeIssueCopy(issue);
|
||||||
else if (action === "edit") handleEditIssue(issue);
|
else if (action === "edit") handleEditIssue(issue);
|
||||||
else if (action === "delete") handleDeleteIssue(issue);
|
else if (action === "delete") handleDeleteIssue(issue);
|
||||||
|
@ -16,8 +16,6 @@ import { Popover, Transition } from "@headlessui/react";
|
|||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// icons
|
|
||||||
import { Component } from "lucide-react";
|
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabels } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -146,10 +144,10 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
|||||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Component
|
<span
|
||||||
className="h-4 w-4 text-custom-text-100 flex-shrink-0"
|
className="h-4 w-4 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
color: watch("color"),
|
backgroundColor: watch("color"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
@ -43,7 +43,7 @@ export const SingleLabel: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<RectangleGroupIcon className="h-4 w-4" />
|
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
<span>Convert to group</span>
|
<span>Convert to group</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
|
@ -204,7 +204,7 @@ export const ProfileIssuesView = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleIssueAction = useCallback(
|
const handleIssueAction = useCallback(
|
||||||
(issue: IIssue, action: "copy" | "edit" | "delete") => {
|
(issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
|
||||||
if (action === "copy") makeIssueCopy(issue);
|
if (action === "copy") makeIssueCopy(issue);
|
||||||
else if (action === "edit") handleEditIssue(issue);
|
else if (action === "edit") handleEditIssue(issue);
|
||||||
else if (action === "delete") handleDeleteIssue(issue);
|
else if (action === "delete") handleDeleteIssue(issue);
|
||||||
|
@ -79,7 +79,7 @@ const ConfirmProjectMemberRemove: React.FC<Props> = ({ isOpen, onClose, data, ha
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 bg-custom-background-90 p-4 sm:px-6">
|
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
||||||
{isDeleteLoading ? "Removing..." : "Remove"}
|
{isDeleteLoading ? "Removing..." : "Remove"}
|
||||||
|
@ -49,7 +49,7 @@ export const MemberSelect: React.FC<Props> = ({ value, onChange }) => {
|
|||||||
{selectedOption ? (
|
{selectedOption ? (
|
||||||
selectedOption?.display_name
|
selectedOption?.display_name
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm py-0.5 text-custom-text-200">Select</span>
|
<span className="text-sm py-0.5 text-custom-sidebar-text-400">Select</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -219,7 +219,9 @@ const SendProjectInvitationModal: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>Select co-worker</div>
|
<div className="flex items-center gap-2 py-0.5">
|
||||||
|
Select co-worker
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@ -249,10 +251,13 @@ const SendProjectInvitationModal: React.FC<Props> = (props) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
{...field}
|
{...field}
|
||||||
label={
|
customButton={
|
||||||
<span className="capitalize">
|
<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">
|
||||||
{field.value ? ROLE[field.value] : "Select role"}
|
<span className="capitalize">
|
||||||
</span>
|
{field.value ? ROLE[field.value] : "Select role"}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
input
|
input
|
||||||
width="w-full"
|
width="w-full"
|
||||||
|
@ -2,7 +2,11 @@ import React from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export const SettingsSidebar = () => {
|
type Props = {
|
||||||
|
profilePage?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsSidebar: React.FC<Props> = ({ profilePage = false }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -47,30 +51,107 @@ export const SettingsSidebar = () => {
|
|||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/custom-objects`,
|
href: `/${workspaceSlug}/projects/${projectId}/settings/custom-objects`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const workspaceLinks: Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
label: "General",
|
||||||
|
href: `/${workspaceSlug}/settings`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Members",
|
||||||
|
href: `/${workspaceSlug}/settings/members`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Billing & Plans",
|
||||||
|
href: `/${workspaceSlug}/settings/billing`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Integrations",
|
||||||
|
href: `/${workspaceSlug}/settings/integrations`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Imports",
|
||||||
|
href: `/${workspaceSlug}/settings/imports`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Exports",
|
||||||
|
href: `/${workspaceSlug}/settings/exports`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const profileLinks: Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
label: "Profile",
|
||||||
|
href: `/${workspaceSlug}/me/profile`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Activity",
|
||||||
|
href: `/${workspaceSlug}/me/profile/activity`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Preferences",
|
||||||
|
href: `/${workspaceSlug}/me/profile/preferences`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 w-80 px-9">
|
<div className="flex flex-col gap-6 w-80 px-5">
|
||||||
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-col gap-1 w-full">
|
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
|
||||||
{projectLinks.map((link) => (
|
<div className="flex flex-col gap-1 w-full">
|
||||||
<Link key={link.href} href={link.href}>
|
{(projectId ? projectLinks : workspaceLinks).map((link) => (
|
||||||
<a>
|
<Link key={link.href} href={link.href}>
|
||||||
<div
|
<a>
|
||||||
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
<div
|
||||||
(
|
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||||
link.label === "Import"
|
(
|
||||||
? router.asPath.includes(link.href)
|
link.label === "Import"
|
||||||
: router.asPath === link.href
|
? router.asPath.includes(link.href)
|
||||||
)
|
: router.asPath === link.href
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
)
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
}`}
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
>
|
}`}
|
||||||
{link.label}
|
>
|
||||||
</div>
|
{link.label}
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
</a>
|
||||||
))}
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!projectId && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-xs text-custom-sidebar-text-400 font-semibold">My Account</span>
|
||||||
|
<div className="flex flex-col gap-1 w-full">
|
||||||
|
{profileLinks.map((link) => (
|
||||||
|
<Link key={link.href} href={link.href}>
|
||||||
|
<a>
|
||||||
|
<div
|
||||||
|
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||||
|
(
|
||||||
|
link.label === "Import"
|
||||||
|
? router.asPath.includes(link.href)
|
||||||
|
: router.asPath === link.href
|
||||||
|
)
|
||||||
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
PhotoFilterOutlined,
|
PhotoFilterOutlined,
|
||||||
SettingsOutlined,
|
SettingsOutlined,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
import { PenSquare } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// types
|
// types
|
||||||
@ -288,6 +289,16 @@ export const SingleSidebarProject: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/${workspaceSlug}/projects/${project?.id}/draft-issues`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<PenSquare className="!text-base !leading-4 w-[14px] h-[14px] text-custom-text-300" />
|
||||||
|
<span>Draft Issues</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)}
|
||||||
>
|
>
|
||||||
|
@ -184,19 +184,20 @@ export const SingleState: React.FC<Props> = ({
|
|||||||
<ArrowDownIcon className="h-4 w-4" />
|
<ArrowDownIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{state.default ? (
|
|
||||||
<span className="text-xs text-custom-text-200">Default</span>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hidden text-xs text-custom-sidebar-text-400 group-hover:inline-block"
|
|
||||||
onClick={handleMakeDefault}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Mark as default
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className=" items-center gap-2.5 hidden group-hover:flex">
|
<div className=" items-center gap-2.5 hidden group-hover:flex">
|
||||||
|
{state.default ? (
|
||||||
|
<span className="text-xs text-custom-text-200">Default</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hidden text-xs text-custom-sidebar-text-400 group-hover:inline-block"
|
||||||
|
onClick={handleMakeDefault}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Mark as default
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center group-hover:opacity-100 opacity-0"
|
className="grid place-items-center group-hover:opacity-100 opacity-0"
|
||||||
@ -215,14 +216,26 @@ export const SingleState: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{state.default ? (
|
{state.default ? (
|
||||||
<Tooltip tooltipContent="Cannot delete the default state.">
|
<Tooltip tooltipContent="Cannot delete the default state.">
|
||||||
<X className="h-3.5 w-3.5 text-red-500" />
|
<X
|
||||||
|
className={`h-4 w-4 ${
|
||||||
|
groupLength < 1 ? "text-custom-sidebar-text-400" : "text-red-500"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : groupLength === 1 ? (
|
) : groupLength === 1 ? (
|
||||||
<Tooltip tooltipContent="Cannot have an empty group.">
|
<Tooltip tooltipContent="Cannot have an empty group.">
|
||||||
<X className="h-3.5 w-3.5 text-red-500" />
|
<X
|
||||||
|
className={`h-4 w-4 ${
|
||||||
|
groupLength < 1 ? "text-custom-sidebar-text-400" : "text-red-500"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<X className="h-3.5 w-3.5 text-red-500" />
|
<X
|
||||||
|
className={`h-4 w-4 ${
|
||||||
|
groupLength < 1 ? "text-custom-sidebar-text-400" : "text-red-500"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
|
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
|
||||||
<div className="flex flex-col items-start gap-3 py-3.5 border-b border-custom-border-200">
|
<div className="flex items-start gap-3 py-3.5 border-b border-custom-border-200">
|
||||||
<h3 className="text-xl font-medium">{bannerName}</h3>
|
<h3 className="text-xl font-medium">{bannerName}</h3>
|
||||||
{description && (
|
{description && (
|
||||||
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">
|
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">
|
||||||
|
@ -73,7 +73,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
archived_at: {
|
archived_at: {
|
||||||
@ -81,7 +81,7 @@ const activityDetails: {
|
|||||||
if (activity.new_value === "restore") return "restored the issue.";
|
if (activity.new_value === "restore") return "restored the issue.";
|
||||||
else return "archived the issue.";
|
else return "archived the issue.";
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
attachment: {
|
attachment: {
|
||||||
@ -99,7 +99,7 @@ const activityDetails: {
|
|||||||
{showIssue && <IssueLink activity={activity} />}
|
{showIssue && <IssueLink activity={activity} />}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
blocking: {
|
blocking: {
|
||||||
@ -156,7 +156,7 @@ const activityDetails: {
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
description: {
|
description: {
|
||||||
@ -172,7 +172,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
estimate_point: {
|
estimate_point: {
|
||||||
@ -190,7 +190,7 @@ const activityDetails: {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
issue: {
|
issue: {
|
||||||
@ -198,7 +198,7 @@ const activityDetails: {
|
|||||||
if (activity.verb === "created") return "created the issue.";
|
if (activity.verb === "created") return "created the issue.";
|
||||||
else return "deleted an issue.";
|
else return "deleted an issue.";
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
labels: {
|
labels: {
|
||||||
@ -225,7 +225,7 @@ const activityDetails: {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
link: {
|
link: {
|
||||||
@ -255,7 +255,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: {
|
modules: {
|
||||||
@ -279,7 +279,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
name: {
|
name: {
|
||||||
@ -295,7 +295,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
parent: {
|
parent: {
|
||||||
@ -314,7 +314,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
priority: {
|
priority: {
|
||||||
@ -333,7 +333,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
start_date: {
|
start_date: {
|
||||||
@ -351,7 +351,7 @@ const activityDetails: {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
|
||||||
state: {
|
state: {
|
||||||
@ -389,7 +389,7 @@ const activityDetails: {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ const ConfirmWorkspaceMemberRemove: React.FC<Props> = ({ isOpen, onClose, data,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 bg-custom-background-90 p-4 sm:px-6">
|
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
||||||
{isDeleteLoading ? "Removing..." : "Remove"}
|
{isDeleteLoading ? "Removing..." : "Remove"}
|
||||||
|
@ -6,7 +6,6 @@ export * from "./help-section";
|
|||||||
export * from "./issues-list";
|
export * from "./issues-list";
|
||||||
export * from "./issues-pie-chart";
|
export * from "./issues-pie-chart";
|
||||||
export * from "./issues-stats";
|
export * from "./issues-stats";
|
||||||
export * from "./settings-header";
|
|
||||||
export * from "./sidebar-dropdown";
|
export * from "./sidebar-dropdown";
|
||||||
export * from "./sidebar-menu";
|
export * from "./sidebar-menu";
|
||||||
export * from "./sidebar-quick-action";
|
export * from "./sidebar-quick-action";
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import SettingsNavbar from "layouts/settings-navbar";
|
|
||||||
|
|
||||||
export const SettingsHeader = () => (
|
|
||||||
<div className="mb-8 space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-2xl font-semibold">Workspace Settings</h3>
|
|
||||||
<p className="mt-1 text-sm text-custom-text-200">
|
|
||||||
This information will be displayed to every member of the workspace.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SettingsNavbar />
|
|
||||||
</div>
|
|
||||||
);
|
|
@ -1,8 +1,6 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
// next-themes
|
// next-themes
|
||||||
@ -63,8 +61,6 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
|
|
||||||
const { user, mutateUser } = useUser();
|
const { user, mutateUser } = useUser();
|
||||||
|
|
||||||
const { collapsed: sidebarCollapse } = useThemeHook();
|
|
||||||
|
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -155,7 +151,7 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
{workspaces.length > 0 ? (
|
{workspaces.length > 0 ? (
|
||||||
workspaces.map((workspace) => (
|
workspaces.map((workspace) => (
|
||||||
<Menu.Item key={workspace.id}>
|
<Menu.Item key={workspace.id}>
|
||||||
{({ active }) => (
|
{() => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleWorkspaceNavigation(workspace)}
|
onClick={() => handleWorkspaceNavigation(workspace)}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useTheme from "hooks/use-theme";
|
import useTheme from "hooks/use-theme";
|
||||||
// components
|
// components
|
||||||
import { NotificationPopover } from "components/notifications";
|
import { NotificationPopover } from "components/notifications";
|
||||||
// ui
|
|
||||||
import { Tooltip } from "components/ui";
|
import { Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
|
@ -1,47 +1,145 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { Icon } from "components/ui";
|
import { Icon } from "components/ui";
|
||||||
|
import { ChevronDown, PenSquare } from "lucide-react";
|
||||||
|
// headless ui
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
// hooks
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
|
// components
|
||||||
|
import { CreateUpdateDraftIssueModal } from "components/issues";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
export const WorkspaceSidebarQuickAction = () => {
|
export const WorkspaceSidebarQuickAction = () => {
|
||||||
const store: any = useMobxStore();
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
return (
|
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
|
||||||
<div
|
|
||||||
className={`flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
|
|
||||||
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className={`flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 ${
|
|
||||||
store?.theme?.sidebarCollapsed
|
|
||||||
? "px-2 hover:bg-custom-sidebar-background-80"
|
|
||||||
: "px-3 shadow border-[0.5px] border-custom-border-300"
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon iconName="edit_square" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
|
|
||||||
{!store?.theme?.sidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", null);
|
||||||
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
|
|
||||||
store?.theme?.sidebarCollapsed
|
return (
|
||||||
? "hover:bg-custom-sidebar-background-80"
|
<>
|
||||||
: "shadow border-[0.5px] border-custom-border-300"
|
<CreateUpdateDraftIssueModal
|
||||||
}`}
|
isOpen={isDraftIssueModalOpen}
|
||||||
onClick={() => {
|
handleClose={() => setIsDraftIssueModalOpen(false)}
|
||||||
const e = new KeyboardEvent("keydown", { key: "k", ctrlKey: true, metaKey: true });
|
prePopulateData={storedValue ? JSON.parse(storedValue) : {}}
|
||||||
document.dispatchEvent(e);
|
onSubmit={() => {
|
||||||
|
localStorage.removeItem("draftedIssue");
|
||||||
|
clearValue();
|
||||||
|
setIsDraftIssueModalOpen(false);
|
||||||
}}
|
}}
|
||||||
|
fieldsToShow={[
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"label",
|
||||||
|
"assignee",
|
||||||
|
"priority",
|
||||||
|
"dueDate",
|
||||||
|
"priority",
|
||||||
|
"state",
|
||||||
|
"startDate",
|
||||||
|
"project",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`relative flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
|
||||||
|
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Icon iconName="search" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
|
<div
|
||||||
</button>
|
className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 ${
|
||||||
</div>
|
store?.theme?.sidebarCollapsed
|
||||||
|
? "px-2 hover:bg-custom-sidebar-background-80"
|
||||||
|
: "px-3 shadow border-[0.5px] border-custom-border-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="edit_square"
|
||||||
|
className="!text-lg !leading-4 text-custom-sidebar-text-300"
|
||||||
|
/>
|
||||||
|
{!store?.theme?.sidebarCollapsed && (
|
||||||
|
<span className="text-sm font-medium">New Issue</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{storedValue && <div className="h-8 w-0.5 bg-custom-sidebar-background-80" />}
|
||||||
|
|
||||||
|
{storedValue && (
|
||||||
|
<div className="relative">
|
||||||
|
<Menu as={React.Fragment}>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Menu.Button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center justify-center rounded flex-shrink-0 p-1.5 ${
|
||||||
|
open ? "rotate-180 pl-0" : "rotate-0 pr-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className="!text-custom-sidebar-text-300 transform transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
<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 -right-4 mt-1 w-52 bg-custom-background-300">
|
||||||
|
<div className="px-1 py-1 ">
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsDraftIssueModalOpen(true)}
|
||||||
|
className="w-full flex text-sm items-center rounded flex-shrink-0 py-[10px] px-3 bg-custom-background-100 shadow border-[0.5px] border-custom-border-300 text-custom-text-300"
|
||||||
|
>
|
||||||
|
<PenSquare
|
||||||
|
size={16}
|
||||||
|
className="!text-lg !leading-4 text-custom-sidebar-text-300 mr-2"
|
||||||
|
/>
|
||||||
|
Last Drafted Issue
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
|
||||||
|
store?.theme?.sidebarCollapsed
|
||||||
|
? "hover:bg-custom-sidebar-background-80"
|
||||||
|
: "shadow border-[0.5px] border-custom-border-300"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "k", ctrlKey: true, metaKey: true });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="search" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -52,7 +52,7 @@ const SingleInvitation: React.FC<Props> = ({
|
|||||||
? "bg-custom-background-80 text-custom-text-200"
|
? "bg-custom-background-80 text-custom-text-200"
|
||||||
: "bg-custom-primary text-white"
|
: "bg-custom-primary text-white"
|
||||||
} text-sm px-4 py-2 border border-custom-border-200 rounded-3xl`}
|
} text-sm px-4 py-2 border border-custom-border-200 rounded-3xl`}
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
handleInvitation(
|
handleInvitation(
|
||||||
invitation,
|
invitation,
|
||||||
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"
|
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"
|
||||||
|
@ -140,6 +140,15 @@ export const PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS = (projectId: string, para
|
|||||||
|
|
||||||
return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
|
return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => {
|
||||||
|
if (!params) return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}`;
|
||||||
|
|
||||||
|
const paramsKey = paramsToKey(params);
|
||||||
|
|
||||||
|
return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}_${paramsKey}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
|
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
|
||||||
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
||||||
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
||||||
|
@ -17,7 +17,6 @@ export const debounce = (func: any, wait: number, immediate: boolean = false) =>
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const API_BASE_URL =
|
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||||
process.env.NEXT_PUBLIC_API_BASE_URL !== undefined
|
? process.env.NEXT_PUBLIC_API_BASE_URL
|
||||||
? process.env.NEXT_PUBLIC_API_BASE_URL
|
: "";
|
||||||
: "http://localhost:8000";
|
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
CYCLE_ISSUES_WITH_PARAMS,
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
STATES_LIST,
|
STATES_LIST,
|
||||||
VIEW_ISSUES,
|
VIEW_ISSUES,
|
||||||
@ -38,6 +39,7 @@ const useIssuesView = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId, archivedIssueId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId, archivedIssueId } = router.query;
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
const isDraftIssues = router.pathname.includes("draft-issues");
|
||||||
|
|
||||||
const params: any = {
|
const params: any = {
|
||||||
order_by: displayFilters?.order_by,
|
order_by: displayFilters?.order_by,
|
||||||
@ -72,6 +74,15 @@ const useIssuesView = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: draftIssues, mutate: mutateDraftIssues } = useSWR(
|
||||||
|
workspaceSlug && projectId && params && isDraftIssues && !archivedIssueId
|
||||||
|
? PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && params && isDraftIssues && !archivedIssueId
|
||||||
|
? () => issuesService.getDraftIssues(workspaceSlug as string, projectId as string, params)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const { data: cycleIssues, mutate: mutateCycleIssues } = useSWR(
|
const { data: cycleIssues, mutate: mutateCycleIssues } = useSWR(
|
||||||
workspaceSlug && projectId && cycleId && params
|
workspaceSlug && projectId && cycleId && params
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
||||||
@ -151,6 +162,8 @@ const useIssuesView = () => {
|
|||||||
? viewIssues
|
? viewIssues
|
||||||
: isArchivedIssues
|
: isArchivedIssues
|
||||||
? projectArchivedIssues
|
? projectArchivedIssues
|
||||||
|
: isDraftIssues
|
||||||
|
? draftIssues
|
||||||
: projectIssues;
|
: projectIssues;
|
||||||
|
|
||||||
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
||||||
@ -169,6 +182,8 @@ const useIssuesView = () => {
|
|||||||
moduleId,
|
moduleId,
|
||||||
viewId,
|
viewId,
|
||||||
isArchivedIssues,
|
isArchivedIssues,
|
||||||
|
isDraftIssues,
|
||||||
|
draftIssues,
|
||||||
emptyStatesObject,
|
emptyStatesObject,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -191,6 +206,8 @@ const useIssuesView = () => {
|
|||||||
? mutateViewIssues
|
? mutateViewIssues
|
||||||
: isArchivedIssues
|
: isArchivedIssues
|
||||||
? mutateProjectArchivedIssues
|
? mutateProjectArchivedIssues
|
||||||
|
: isDraftIssues
|
||||||
|
? mutateDraftIssues
|
||||||
: mutateProjectIssues,
|
: mutateProjectIssues,
|
||||||
filters,
|
filters,
|
||||||
setFilters,
|
setFilters,
|
||||||
|
@ -1,127 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
profilePage?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SettingsNavbar: React.FC<Props> = ({ profilePage = false }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const workspaceLinks: Array<{
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
label: "General",
|
|
||||||
href: `/${workspaceSlug}/settings`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Members",
|
|
||||||
href: `/${workspaceSlug}/settings/members`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Billing & Plans",
|
|
||||||
href: `/${workspaceSlug}/settings/billing`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Integrations",
|
|
||||||
href: `/${workspaceSlug}/settings/integrations`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Imports",
|
|
||||||
href: `/${workspaceSlug}/settings/imports`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Exports",
|
|
||||||
href: `/${workspaceSlug}/settings/exports`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const projectLinks: Array<{
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
label: "General",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Control",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/control`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Members",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/members`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Features",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/features`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "States",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/states`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Labels",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Integrations",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/integrations`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Estimates",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Automations",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const profileLinks: Array<{
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
label: "General",
|
|
||||||
href: `/${workspaceSlug}/me/profile`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Activity",
|
|
||||||
href: `/${workspaceSlug}/me/profile/activity`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Preferences",
|
|
||||||
href: `/${workspaceSlug}/me/profile/preferences`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-4">
|
|
||||||
{(profilePage ? profileLinks : projectId ? projectLinks : workspaceLinks).map((link) => (
|
|
||||||
<Link key={link.href} href={link.href}>
|
|
||||||
<a>
|
|
||||||
<div
|
|
||||||
className={`rounded-full border px-5 py-1.5 text-sm outline-none ${
|
|
||||||
(
|
|
||||||
link.label === "Import"
|
|
||||||
? router.asPath.includes(link.href)
|
|
||||||
: router.asPath === link.href
|
|
||||||
)
|
|
||||||
? "border-custom-primary bg-custom-primary text-white"
|
|
||||||
: "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsNavbar;
|
|
@ -3,8 +3,6 @@ import { useEffect } from "react";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// theme helpers
|
|
||||||
import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
|
|
||||||
|
|
||||||
const MobxStoreInit = () => {
|
const MobxStoreInit = () => {
|
||||||
const store: any = useMobxStore();
|
const store: any = useMobxStore();
|
||||||
|
@ -2,6 +2,7 @@ require("dotenv").config({ path: ".env" });
|
|||||||
|
|
||||||
const { withSentryConfig } = require("@sentry/nextjs");
|
const { withSentryConfig } = require("@sentry/nextjs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const extraImageDomains = (process.env.NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS ?? "")
|
const extraImageDomains = (process.env.NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS ?? "")
|
||||||
.split(",")
|
.split(",")
|
||||||
.filter((domain) => domain.length > 0);
|
.filter((domain) => domain.length > 0);
|
||||||
|
@ -97,8 +97,8 @@
|
|||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"eslint-config-next": "12.2.2",
|
"eslint-config-next": "12.2.2",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
"tsconfig": "*",
|
|
||||||
"tailwind-config-custom": "*",
|
"tailwind-config-custom": "*",
|
||||||
|
"tsconfig": "*",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
@ -7,7 +7,6 @@ import Link from "next/link";
|
|||||||
import userService from "services/user.service";
|
import userService from "services/user.service";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
import SettingsNavbar from "layouts/settings-navbar";
|
|
||||||
// components
|
// components
|
||||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||||
import { TipTapEditor } from "components/tiptap";
|
import { TipTapEditor } from "components/tiptap";
|
||||||
@ -20,6 +19,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
|||||||
import { USER_ACTIVITY } from "constants/fetch-keys";
|
import { USER_ACTIVITY } from "constants/fetch-keys";
|
||||||
// helper
|
// helper
|
||||||
import { timeAgo } from "helpers/date-time.helper";
|
import { timeAgo } from "helpers/date-time.helper";
|
||||||
|
import { SettingsSidebar } from "components/project";
|
||||||
|
|
||||||
const ProfileActivity = () => {
|
const ProfileActivity = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -38,186 +38,179 @@ const ProfileActivity = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2">
|
||||||
<div className="mb-8 space-y-6">
|
<div className="w-80 py-8">
|
||||||
<div>
|
<SettingsSidebar />
|
||||||
<h3 className="text-3xl font-semibold">Profile Settings</h3>
|
|
||||||
<p className="mt-1 text-custom-text-200">
|
|
||||||
This information will be visible to only you.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SettingsNavbar profilePage />
|
|
||||||
</div>
|
</div>
|
||||||
{userActivity ? (
|
|
||||||
<div>
|
|
||||||
<ul role="list" className="-mb-4">
|
|
||||||
{userActivity.results.map((activityItem: any, activityIdx: number) => {
|
|
||||||
if (activityItem.field === "comment") {
|
|
||||||
return (
|
|
||||||
<div key={activityItem.id} className="mt-2">
|
|
||||||
<div className="relative flex items-start space-x-3">
|
|
||||||
<div className="relative px-1">
|
|
||||||
{activityItem.field ? (
|
|
||||||
activityItem.new_value === "restore" && (
|
|
||||||
<Icon iconName="history" className="text-sm text-custom-text-200" />
|
|
||||||
)
|
|
||||||
) : activityItem.actor_detail.avatar &&
|
|
||||||
activityItem.actor_detail.avatar !== "" ? (
|
|
||||||
<img
|
|
||||||
src={activityItem.actor_detail.avatar}
|
|
||||||
alt={activityItem.actor_detail.display_name}
|
|
||||||
height={30}
|
|
||||||
width={30}
|
|
||||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
|
|
||||||
>
|
|
||||||
{activityItem.actor_detail.display_name?.charAt(0)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
{userActivity ? (
|
||||||
<ChatBubbleLeftEllipsisIcon
|
<section className="pr-9 py-8 w-full">
|
||||||
className="h-3.5 w-3.5 text-custom-text-200"
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
aria-hidden="true"
|
<h3 className="text-xl font-medium">Acitivity</h3>
|
||||||
/>
|
</div>
|
||||||
</span>
|
<div className={`flex flex-col gap-2 py-4 w-full`}>
|
||||||
</div>
|
<ul role="list" className="-mb-4">
|
||||||
<div className="min-w-0 flex-1">
|
{userActivity.results.map((activityItem: any, activityIdx: number) => {
|
||||||
<div>
|
if (activityItem.field === "comment") {
|
||||||
<div className="text-xs">
|
return (
|
||||||
{activityItem.actor_detail.is_bot
|
<div key={activityItem.id} className="mt-2">
|
||||||
? activityItem.actor_detail.first_name + " Bot"
|
<div className="relative flex items-start space-x-3">
|
||||||
: activityItem.actor_detail.display_name}
|
<div className="relative px-1">
|
||||||
</div>
|
{activityItem.field ? (
|
||||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
activityItem.new_value === "restore" && (
|
||||||
Commented {timeAgo(activityItem.created_at)}
|
<Icon iconName="history" className="text-sm text-custom-text-200" />
|
||||||
</p>
|
)
|
||||||
|
) : activityItem.actor_detail.avatar &&
|
||||||
|
activityItem.actor_detail.avatar !== "" ? (
|
||||||
|
<img
|
||||||
|
src={activityItem.actor_detail.avatar}
|
||||||
|
alt={activityItem.actor_detail.display_name}
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
|
||||||
|
>
|
||||||
|
{activityItem.actor_detail.display_name?.charAt(0)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||||
|
<ChatBubbleLeftEllipsisIcon
|
||||||
|
className="h-6 w-6 !text-2xl text-custom-text-200"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="issue-comments-section p-0">
|
<div className="min-w-0 flex-1">
|
||||||
<TipTapEditor
|
<div>
|
||||||
workspaceSlug={workspaceSlug as string}
|
<div className="text-xs">
|
||||||
value={
|
{activityItem.actor_detail.is_bot
|
||||||
activityItem?.new_value !== ""
|
? activityItem.actor_detail.first_name + " Bot"
|
||||||
? activityItem.new_value
|
: activityItem.actor_detail.display_name}
|
||||||
: activityItem.old_value
|
</div>
|
||||||
}
|
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
Commented {timeAgo(activityItem.created_at)}
|
||||||
noBorder
|
</p>
|
||||||
borderOnFocus={false}
|
</div>
|
||||||
editable={false}
|
<div className="issue-comments-section p-0">
|
||||||
/>
|
<TipTapEditor
|
||||||
|
workspaceSlug={workspaceSlug as string}
|
||||||
|
value={
|
||||||
|
activityItem?.new_value !== ""
|
||||||
|
? activityItem.new_value
|
||||||
|
: activityItem.old_value
|
||||||
|
}
|
||||||
|
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||||
|
noBorder
|
||||||
|
borderOnFocus={false}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
activityItem.verb === "created" &&
|
activityItem.verb === "created" &&
|
||||||
activityItem.field !== "cycles" &&
|
activityItem.field !== "cycles" &&
|
||||||
activityItem.field !== "modules" &&
|
activityItem.field !== "modules" &&
|
||||||
activityItem.field !== "attachment" &&
|
activityItem.field !== "attachment" &&
|
||||||
activityItem.field !== "link" &&
|
activityItem.field !== "link" &&
|
||||||
activityItem.field !== "estimate" ? (
|
activityItem.field !== "estimate" ? (
|
||||||
<span className="text-custom-text-200">
|
<span className="text-custom-text-200">
|
||||||
created{" "}
|
created{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}
|
href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}
|
||||||
>
|
>
|
||||||
<a className="inline-flex items-center hover:underline">
|
<a className="inline-flex items-center hover:underline">
|
||||||
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
|
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
) : activityItem.field ? (
|
) : activityItem.field ? (
|
||||||
<ActivityMessage activity={activityItem} showIssue />
|
<ActivityMessage activity={activityItem} showIssue />
|
||||||
) : (
|
) : (
|
||||||
"created the issue."
|
"created the issue."
|
||||||
);
|
);
|
||||||
|
|
||||||
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||||
return (
|
return (
|
||||||
<li key={activityItem.id}>
|
<li key={activityItem.id}>
|
||||||
<div className="relative pb-1">
|
<div className="relative pb-1">
|
||||||
{userActivity.results.length > 1 &&
|
<div className="relative flex items-center space-x-2">
|
||||||
activityIdx !== userActivity.results.length - 1 ? (
|
<>
|
||||||
<span
|
<div>
|
||||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
<div className="relative px-1.5">
|
||||||
aria-hidden="true"
|
<div className="mt-1.5">
|
||||||
/>
|
<div className="flex h-6 w-6 items-center justify-center">
|
||||||
) : null}
|
{activityItem.field ? (
|
||||||
<div className="relative flex items-start space-x-2">
|
activityItem.new_value === "restore" ? (
|
||||||
<>
|
<Icon
|
||||||
<div>
|
iconName="history"
|
||||||
<div className="relative px-1.5">
|
className="!text-2xl text-custom-text-200"
|
||||||
<div className="mt-1.5">
|
/>
|
||||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
) : (
|
||||||
{activityItem.field ? (
|
<ActivityIcon activity={activityItem} />
|
||||||
activityItem.new_value === "restore" ? (
|
)
|
||||||
<Icon
|
) : activityItem.actor_detail.avatar &&
|
||||||
iconName="history"
|
activityItem.actor_detail.avatar !== "" ? (
|
||||||
className="text-sm text-custom-text-200"
|
<img
|
||||||
|
src={activityItem.actor_detail.avatar}
|
||||||
|
alt={activityItem.actor_detail.display_name}
|
||||||
|
height={24}
|
||||||
|
width={24}
|
||||||
|
className="rounded-full"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ActivityIcon activity={activityItem} />
|
<div
|
||||||
)
|
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||||
) : activityItem.actor_detail.avatar &&
|
>
|
||||||
activityItem.actor_detail.avatar !== "" ? (
|
{activityItem.actor_detail.display_name?.charAt(0)}
|
||||||
<img
|
</div>
|
||||||
src={activityItem.actor_detail.avatar}
|
)}
|
||||||
alt={activityItem.actor_detail.display_name}
|
</div>
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
className="rounded-full"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
|
||||||
>
|
|
||||||
{activityItem.actor_detail.display_name?.charAt(0)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="min-w-0 flex-1 py-4 border-b border-custom-border-200">
|
||||||
<div className="min-w-0 flex-1 py-3">
|
<div className="text-sm text-custom-text-200 break-words">
|
||||||
<div className="text-xs text-custom-text-200 break-words">
|
{activityItem.field === "archived_at" &&
|
||||||
{activityItem.field === "archived_at" &&
|
activityItem.new_value !== "restore" ? (
|
||||||
activityItem.new_value !== "restore" ? (
|
<span className="text-gray font-medium">Plane</span>
|
||||||
<span className="text-gray font-medium">Plane</span>
|
) : activityItem.actor_detail.is_bot ? (
|
||||||
) : activityItem.actor_detail.is_bot ? (
|
<span className="text-gray font-medium">
|
||||||
<span className="text-gray font-medium">
|
{activityItem.actor_detail.first_name} Bot
|
||||||
{activityItem.actor_detail.first_name} Bot
|
</span>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
|
||||||
|
>
|
||||||
|
<a className="text-gray font-medium">
|
||||||
|
{activityItem.actor_detail.display_name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}{" "}
|
||||||
|
{message}{" "}
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
|
{timeAgo(activityItem.created_at)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
</div>
|
||||||
<Link
|
|
||||||
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
|
|
||||||
>
|
|
||||||
<a className="text-gray font-medium">
|
|
||||||
{activityItem.actor_detail.display_name}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
)}{" "}
|
|
||||||
{message}{" "}
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{timeAgo(activityItem.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
);
|
||||||
);
|
}
|
||||||
}
|
})}
|
||||||
})}
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-5">
|
<Loader className="space-y-5">
|
||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
@ -10,21 +12,15 @@ import useUserAuth from "hooks/use-user-auth";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
import SettingsNavbar from "layouts/settings-navbar";
|
|
||||||
// components
|
// components
|
||||||
import { ImagePickerPopover, ImageUploadModal } from "components/core";
|
import { ImagePickerPopover, ImageUploadModal } from "components/core";
|
||||||
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import {
|
import { CustomSearchSelect, CustomSelect, Input, PrimaryButton, Spinner } from "components/ui";
|
||||||
CustomSearchSelect,
|
|
||||||
CustomSelect,
|
|
||||||
DangerButton,
|
|
||||||
Input,
|
|
||||||
SecondaryButton,
|
|
||||||
Spinner,
|
|
||||||
} from "components/ui";
|
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { UserIcon } from "@heroicons/react/24/outline";
|
import { UserIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { UserCircle } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import type { IUser } from "types";
|
import type { IUser } from "types";
|
||||||
@ -46,6 +42,9 @@ const Profile: NextPage = () => {
|
|||||||
const [isRemoving, setIsRemoving] = useState(false);
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -126,6 +125,7 @@ const Profile: NextPage = () => {
|
|||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
return { ...prevData, avatar: "" };
|
return { ...prevData, avatar: "" };
|
||||||
}, false);
|
}, false);
|
||||||
|
setIsRemoving(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -155,6 +155,8 @@ const Profile: NextPage = () => {
|
|||||||
<ImageUploadModal
|
<ImageUploadModal
|
||||||
isOpen={isImageUploadModalOpen}
|
isOpen={isImageUploadModalOpen}
|
||||||
onClose={() => setIsImageUploadModalOpen(false)}
|
onClose={() => setIsImageUploadModalOpen(false)}
|
||||||
|
isRemoving={isRemoving}
|
||||||
|
handleDelete={() => handleDelete(myProfile?.avatar, true)}
|
||||||
onSuccess={(url) => {
|
onSuccess={(url) => {
|
||||||
setValue("avatar", url);
|
setValue("avatar", url);
|
||||||
handleSubmit(onSubmit)();
|
handleSubmit(onSubmit)();
|
||||||
@ -164,81 +166,49 @@ const Profile: NextPage = () => {
|
|||||||
userImage
|
userImage
|
||||||
/>
|
/>
|
||||||
{myProfile ? (
|
{myProfile ? (
|
||||||
<div className="p-8">
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="mb-8 space-y-6">
|
<div className="flex flex-row gap-2">
|
||||||
<div>
|
<div className="w-80 py-8">
|
||||||
<h3 className="text-3xl font-semibold">Profile Settings</h3>
|
<SettingsSidebar />
|
||||||
<p className="mt-1 text-custom-text-200">
|
|
||||||
This information will be visible to only you.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<SettingsNavbar profilePage />
|
<div className={`flex flex-col gap-8 pr-9 py-9 w-full`}>
|
||||||
</div>
|
<div className="relative h-44 w-full mt-6">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8 sm:space-y-12">
|
<img
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
src={
|
||||||
<div className="col-span-12 sm:col-span-6">
|
watch("cover_image") ??
|
||||||
<h4 className="text-lg font-semibold text-custom-text-100">Profile Picture</h4>
|
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
|
||||||
<p className="text-sm text-custom-text-200">
|
}
|
||||||
Max file size is 5MB. Supported file types are .jpg and .png.
|
className="h-44 w-full rounded-lg object-cover"
|
||||||
</p>
|
alt={myProfile?.name ?? "Cover image"}
|
||||||
</div>
|
/>
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="flex items-end justify-between absolute left-8 -bottom-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex gap-3">
|
||||||
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
|
<div className="flex items-center justify-center bg-custom-background-90 h-16 w-16 rounded-lg">
|
||||||
{!watch("avatar") || watch("avatar") === "" ? (
|
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
|
||||||
<div className="h-12 w-12 rounded-md bg-custom-background-80 p-2">
|
{!watch("avatar") || watch("avatar") === "" ? (
|
||||||
<UserIcon className="h-full w-full text-custom-text-200" />
|
<div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
|
||||||
</div>
|
<UserIcon className="h-full w-full text-custom-text-200" />
|
||||||
) : (
|
</div>
|
||||||
<div className="relative h-12 w-12 overflow-hidden">
|
) : (
|
||||||
<img
|
<div className="relative h-16 w-16 overflow-hidden">
|
||||||
src={watch("avatar")}
|
<img
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
src={watch("avatar")}
|
||||||
onClick={() => setIsImageUploadModalOpen(true)}
|
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
|
||||||
alt={myProfile.display_name}
|
onClick={() => setIsImageUploadModalOpen(true)}
|
||||||
/>
|
alt={myProfile.display_name}
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
</button>
|
)}
|
||||||
<div className="flex items-center gap-2">
|
</button>
|
||||||
<SecondaryButton
|
</div>
|
||||||
onClick={() => {
|
|
||||||
setIsImageUploadModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</SecondaryButton>
|
|
||||||
{myProfile.avatar && myProfile.avatar !== "" && (
|
|
||||||
<DangerButton
|
|
||||||
onClick={() => handleDelete(myProfile.avatar, true)}
|
|
||||||
loading={isRemoving}
|
|
||||||
>
|
|
||||||
{isRemoving ? "Removing..." : "Remove"}
|
|
||||||
</DangerButton>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<div className="flex absolute right-3 bottom-3">
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
<Controller
|
||||||
<div className="col-span-12 sm:col-span-6">
|
control={control}
|
||||||
<h4 className="text-lg font-semibold">Cover Photo</h4>
|
name="cover_image"
|
||||||
<p className="text-sm text-custom-text-200">
|
render={() => (
|
||||||
Select your cover photo from the given library.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<div className="h-32 w-full rounded border border-custom-border-200 p-1">
|
|
||||||
<div className="relative h-full w-full rounded">
|
|
||||||
<img
|
|
||||||
src={
|
|
||||||
watch("cover_image") ??
|
|
||||||
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
|
|
||||||
}
|
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover rounded"
|
|
||||||
alt={myProfile?.name ?? "Cover image"}
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-0 flex w-full justify-end">
|
|
||||||
<ImagePickerPopover
|
<ImagePickerPopover
|
||||||
label={"Change cover"}
|
label={"Change cover"}
|
||||||
onChange={(imageUrl) => {
|
onChange={(imageUrl) => {
|
||||||
@ -249,157 +219,167 @@ const Profile: NextPage = () => {
|
|||||||
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
|
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex item-center justify-between px-8 mt-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex item-center text-lg font-semibold text-custom-text-100">
|
||||||
|
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-sm tracking-tight">{watch("email")}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link href={`/${workspaceSlug}/profile/${myProfile.id}`}>
|
||||||
|
<a className="flex item-center cursor-pointer gap-2 h-4 leading-4 text-sm text-custom-primary-100">
|
||||||
|
<span className="h-4 w-4">
|
||||||
|
<UserCircle className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
View Profile
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm">First Name</h4>
|
||||||
|
<Input
|
||||||
|
name="first_name"
|
||||||
|
id="first_name"
|
||||||
|
register={register}
|
||||||
|
error={errors.first_name}
|
||||||
|
placeholder="Enter your first name"
|
||||||
|
className="!px-3 !py-2 rounded-md font-medium"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm">Last Name</h4>
|
||||||
|
<Input
|
||||||
|
name="last_name"
|
||||||
|
register={register}
|
||||||
|
error={errors.last_name}
|
||||||
|
id="last_name"
|
||||||
|
placeholder="Enter your last name"
|
||||||
|
autoComplete="off"
|
||||||
|
className="!px-3 !py-2 rounded-md font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm">Email</h4>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
className="!px-3 !py-2 rounded-md font-medium"
|
||||||
|
error={errors.name}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm">Role</h4>
|
||||||
|
<Controller
|
||||||
|
name="role"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: "This field is required" }}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
label={value ? value.toString() : "Select your role"}
|
||||||
|
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}>
|
||||||
|
{item.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.role && (
|
||||||
|
<span className="text-xs text-red-500">Please select a role</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm">Display name </h4>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="display_name"
|
||||||
|
name="display_name"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
error={errors.display_name}
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Enter your display name"
|
||||||
|
validations={{
|
||||||
|
required: "Display name is required.",
|
||||||
|
validate: (value) => {
|
||||||
|
if (value.trim().length < 1) return "Display name can't be empty.";
|
||||||
|
|
||||||
|
if (value.split(" ").length > 1)
|
||||||
|
return "Display name can't have two consecutive spaces.";
|
||||||
|
|
||||||
|
if (value.replace(/\s/g, "").length < 1)
|
||||||
|
return "Display name must be at least 1 characters long.";
|
||||||
|
|
||||||
|
if (value.replace(/\s/g, "").length > 20)
|
||||||
|
return "Display name must be less than 20 characters long.";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm">Timezone </h4>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="user_timezone"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: "This field is required" }}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<CustomSearchSelect
|
||||||
|
value={value}
|
||||||
|
label={
|
||||||
|
value
|
||||||
|
? TIME_ZONES.find((t) => t.value === value)?.label ?? value
|
||||||
|
: "Select a timezone"
|
||||||
|
}
|
||||||
|
options={timeZoneOptions}
|
||||||
|
onChange={onChange}
|
||||||
|
verticalPosition="top"
|
||||||
|
optionsClassName="w-full"
|
||||||
|
input
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.role && (
|
||||||
|
<span className="text-xs text-red-500">Please select a role</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating Project..." : "Update Project"}
|
||||||
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
</div>
|
||||||
<div className="col-span-12 sm:col-span-6">
|
</form>
|
||||||
<h4 className="text-lg font-semibold text-custom-text-100">Full Name</h4>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
|
|
||||||
<Input
|
|
||||||
name="first_name"
|
|
||||||
id="first_name"
|
|
||||||
register={register}
|
|
||||||
error={errors.first_name}
|
|
||||||
placeholder="Enter your first name"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="last_name"
|
|
||||||
register={register}
|
|
||||||
error={errors.last_name}
|
|
||||||
id="last_name"
|
|
||||||
placeholder="Enter your last name"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold text-custom-text-100">Display Name</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
This could be your first name, or a nickname — however you{"'"}d like people to
|
|
||||||
refer to you in Plane.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<Input
|
|
||||||
id="display_name"
|
|
||||||
name="display_name"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
error={errors.display_name}
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Enter your display name"
|
|
||||||
validations={{
|
|
||||||
required: "Display name is required.",
|
|
||||||
validate: (value) => {
|
|
||||||
if (value.trim().length < 1) return "Display name can't be empty.";
|
|
||||||
|
|
||||||
if (value.split(" ").length > 1)
|
|
||||||
return "Display name can't have two consecutive spaces.";
|
|
||||||
|
|
||||||
if (value.replace(/\s/g, "").length < 1)
|
|
||||||
return "Display name must be at least 1 characters long.";
|
|
||||||
|
|
||||||
if (value.replace(/\s/g, "").length > 20)
|
|
||||||
return "Display name must be less than 20 characters long.";
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold text-custom-text-100">Email</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
The email address that you are using.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
error={errors.name}
|
|
||||||
className="w-full"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold text-custom-text-100">Role</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">Add your role.</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<Controller
|
|
||||||
name="role"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: "This field is required" }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<CustomSelect
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
label={value ? value.toString() : "Select your role"}
|
|
||||||
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""}
|
|
||||||
width="w-full"
|
|
||||||
input
|
|
||||||
position="right"
|
|
||||||
>
|
|
||||||
{USER_ROLES.map((item) => (
|
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
|
||||||
{item.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold text-custom-text-100">Timezone</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">Select a timezone</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<Controller
|
|
||||||
name="user_timezone"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: "This field is required" }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<CustomSearchSelect
|
|
||||||
value={value}
|
|
||||||
label={
|
|
||||||
value
|
|
||||||
? TIME_ZONES.find((t) => t.value === value)?.label ?? value
|
|
||||||
: "Select a timezone"
|
|
||||||
}
|
|
||||||
options={timeZoneOptions}
|
|
||||||
onChange={onChange}
|
|
||||||
verticalPosition="top"
|
|
||||||
optionsClassName="w-full"
|
|
||||||
input
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="sm:text-right">
|
|
||||||
<SecondaryButton type="submit" loading={isSubmitting}>
|
|
||||||
{isSubmitting ? "Updating..." : "Update profile"}
|
|
||||||
</SecondaryButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -3,7 +3,6 @@ import { useEffect, useState } from "react";
|
|||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
import SettingsNavbar from "layouts/settings-navbar";
|
|
||||||
// components
|
// components
|
||||||
import { CustomThemeSelector, ThemeSwitch } from "components/core";
|
import { CustomThemeSelector, ThemeSwitch } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
@ -15,17 +14,15 @@ import { ICustomTheme } from "types";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// next themes
|
import { SettingsSidebar } from "components/project";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
|
||||||
const ProfilePreferences = observer(() => {
|
const ProfilePreferences = observer(() => {
|
||||||
const { user: myProfile } = useUserAuth();
|
const { user: myProfile } = useUserAuth();
|
||||||
|
|
||||||
const store: any = useMobxStore();
|
const store: any = useMobxStore();
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
console.log("store", store?.theme?.theme);
|
// console.log("store", store?.theme?.theme);
|
||||||
console.log("theme", theme);
|
// console.log("theme", theme);
|
||||||
|
|
||||||
const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false);
|
const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false);
|
||||||
|
|
||||||
@ -62,18 +59,16 @@ const ProfilePreferences = observer(() => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{myProfile ? (
|
{myProfile ? (
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2">
|
||||||
<div className="mb-8 space-y-6">
|
<div className="w-80 py-8">
|
||||||
<div>
|
<SettingsSidebar />
|
||||||
<h3 className="text-3xl font-semibold">Profile Settings</h3>
|
|
||||||
<p className="mt-1 text-custom-text-200">
|
|
||||||
This information will be visible to only you.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SettingsNavbar profilePage />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-8 sm:space-y-12">
|
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
<div className="pr-9 py-8 w-full">
|
||||||
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
|
<h3 className="text-xl font-medium">Acitivity</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="col-span-12 sm:col-span-6">
|
||||||
<h4 className="text-lg font-semibold text-custom-text-100">Theme</h4>
|
<h4 className="text-lg font-semibold text-custom-text-100">Theme</h4>
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-sm text-custom-text-200">
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import projectService from "services/project.service";
|
||||||
|
// layouts
|
||||||
|
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||||
|
// contexts
|
||||||
|
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||||
|
// helper
|
||||||
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
// components
|
||||||
|
import { IssuesFilterView, IssuesView } from "components/core";
|
||||||
|
// ui
|
||||||
|
import { Icon } from "components/ui";
|
||||||
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
|
// icons
|
||||||
|
import { X, PenSquare } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import type { NextPage } from "next";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
const ProjectDraftIssues: NextPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { data: projectDetails } = useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IssueViewContextProvider>
|
||||||
|
<ProjectAuthorizationWrapper
|
||||||
|
breadcrumbs={
|
||||||
|
<Breadcrumbs>
|
||||||
|
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||||
|
<BreadcrumbItem
|
||||||
|
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Draft Issues`}
|
||||||
|
/>
|
||||||
|
</Breadcrumbs>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IssuesFilterView />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<div className="flex items-center ga-1 px-4 py-2.5 shadow-sm border-b border-custom-border-200">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||||
|
className="flex items-center gap-1.5 rounded border border-custom-border-200 px-3 py-1.5 text-xs"
|
||||||
|
>
|
||||||
|
<PenSquare className="h-3 w-3 text-custom-text-300" />
|
||||||
|
<span>Draft Issues</span>
|
||||||
|
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<IssuesView />
|
||||||
|
</div>
|
||||||
|
</ProjectAuthorizationWrapper>
|
||||||
|
</IssueViewContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectDraftIssues;
|
@ -19,7 +19,8 @@ import { ToggleSwitch } from "components/ui";
|
|||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { ModuleIcon } from "components/icons";
|
import { ModuleIcon } from "components/icons";
|
||||||
import { Contrast, FileText, Inbox, Layers } from "lucide-react";
|
import { FileText, Inbox, Layers } from "lucide-react";
|
||||||
|
import { ContrastOutlined } from "@mui/icons-material";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
@ -33,7 +34,10 @@ const featuresList = [
|
|||||||
title: "Cycles",
|
title: "Cycles",
|
||||||
description:
|
description:
|
||||||
"Cycles are enabled for all the projects in this workspace. Access them from the sidebar.",
|
"Cycles are enabled for all the projects in this workspace. Access them from the sidebar.",
|
||||||
icon: <Contrast className="h-4 w-4 text-custom-primary-100 flex-shrink-0" />,
|
icon: (
|
||||||
|
<ContrastOutlined className="!text-base !leading-4 text-purple-500 flex-shrink-0 rotate-180" />
|
||||||
|
),
|
||||||
|
|
||||||
property: "cycle_view",
|
property: "cycle_view",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -61,7 +65,7 @@ const featuresList = [
|
|||||||
title: "Inbox",
|
title: "Inbox",
|
||||||
description:
|
description:
|
||||||
"Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
|
"Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
|
||||||
icon: <Inbox className="h-4 w-4 text-cyan-500 flex-shrink-0" />,
|
icon: <Inbox className="h-4 w-4 text-fuchsia-500 flex-shrink-0" />,
|
||||||
property: "inbox_view",
|
property: "inbox_view",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -25,7 +25,6 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
Loader,
|
Loader,
|
||||||
CustomSelect,
|
CustomSelect,
|
||||||
SecondaryButton,
|
|
||||||
DangerButton,
|
DangerButton,
|
||||||
Icon,
|
Icon,
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
@ -67,7 +66,7 @@ const GeneralSettings: NextPage = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: memberDetails, error } = useSWR(
|
const { data: memberDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
|
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
|
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
|
||||||
@ -168,6 +167,7 @@ const GeneralSettings: NextPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
|
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
|
||||||
|
const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
|
||||||
|
|
||||||
const isAdmin = memberDetails?.role === 20;
|
const isAdmin = memberDetails?.role === 20;
|
||||||
|
|
||||||
@ -350,7 +350,7 @@ const GeneralSettings: NextPage = () => {
|
|||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
label={currentNetwork?.label ?? "Select network"}
|
label={selectedNetwork?.label ?? "Select network"}
|
||||||
className="!border-custom-border-200 !shadow-none"
|
className="!border-custom-border-200 !shadow-none"
|
||||||
input
|
input
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
@ -388,59 +388,60 @@ const GeneralSettings: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{isAdmin && (
|
||||||
|
<Disclosure as="div" className="border-t border-custom-border-400">
|
||||||
|
{({ open }) => (
|
||||||
|
<div className="w-full">
|
||||||
|
<Disclosure.Button
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
className="flex items-center justify-between w-full py-4"
|
||||||
|
>
|
||||||
|
<span className="text-xl tracking-tight">Delete Project</span>
|
||||||
|
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
|
||||||
|
</Disclosure.Button>
|
||||||
|
|
||||||
<Disclosure as="div" className="border-t border-custom-border-400">
|
<Transition
|
||||||
{({ open }) => (
|
show={open}
|
||||||
<div className="w-full">
|
enter="transition duration-100 ease-out"
|
||||||
<Disclosure.Button
|
enterFrom="transform opacity-0"
|
||||||
as="button"
|
enterTo="transform opacity-100"
|
||||||
type="button"
|
leave="transition duration-75 ease-out"
|
||||||
className="flex items-center justify-between w-full py-4"
|
leaveFrom="transform opacity-100"
|
||||||
>
|
leaveTo="transform opacity-0"
|
||||||
<span className="text-xl tracking-tight">Danger Zone</span>
|
>
|
||||||
<Icon iconName={open ? "expand_more" : "expand_less"} className="!text-2xl" />
|
<Disclosure.Panel>
|
||||||
</Disclosure.Button>
|
<div className="flex flex-col gap-8">
|
||||||
|
<span className="text-sm tracking-tight">
|
||||||
<Transition
|
The danger zone of the project delete page is a critical area that
|
||||||
show={open}
|
requires careful consideration and attention. When deleting a project,
|
||||||
enter="transition duration-100 ease-out"
|
all of the data and resources within that project will be permanently
|
||||||
enterFrom="transform opacity-0"
|
removed and cannot be recovered.
|
||||||
enterTo="transform opacity-100"
|
</span>
|
||||||
leave="transition duration-75 ease-out"
|
<div>
|
||||||
leaveFrom="transform opacity-100"
|
{projectDetails ? (
|
||||||
leaveTo="transform opacity-0"
|
<div>
|
||||||
>
|
<DangerButton
|
||||||
<Disclosure.Panel>
|
onClick={() => setSelectedProject(projectDetails.id ?? null)}
|
||||||
<div className="flex flex-col gap-8">
|
className="!text-sm"
|
||||||
<span className="text-sm tracking-tight">
|
outline
|
||||||
The danger zone of the project delete page is a critical area that
|
>
|
||||||
requires careful consideration and attention. When deleting a project, all
|
Delete my project
|
||||||
of the data and resources within that project will be permanently removed
|
</DangerButton>
|
||||||
and cannot be recovered.
|
</div>
|
||||||
</span>
|
) : (
|
||||||
<div>
|
<Loader className="mt-2 w-full">
|
||||||
{projectDetails ? (
|
<Loader.Item height="38px" width="144px" />
|
||||||
<div>
|
</Loader>
|
||||||
<DangerButton
|
)}
|
||||||
onClick={() => setSelectedProject(projectDetails.id ?? null)}
|
</div>
|
||||||
className="!text-sm"
|
|
||||||
outline
|
|
||||||
>
|
|
||||||
Delete my project
|
|
||||||
</DangerButton>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Loader className="mt-2 w-full">
|
|
||||||
<Loader.Item height="38px" width="144px" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Disclosure.Panel>
|
||||||
</Disclosure.Panel>
|
</Transition>
|
||||||
</Transition>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Disclosure>
|
||||||
</Disclosure>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -113,11 +113,11 @@ const LabelsSettings: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2 h-full w-full">
|
||||||
<div className="w-80 py-8">
|
<div className="w-80 py-8">
|
||||||
<SettingsSidebar />
|
<SettingsSidebar />
|
||||||
</div>
|
</div>
|
||||||
<section className="pr-9 py-8 gap-10 w-full">
|
<section className="pr-9 py-8 gap-10 h-full w-full">
|
||||||
<div className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200">
|
<div className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200">
|
||||||
<h3 className="text-xl font-medium">Labels</h3>
|
<h3 className="text-xl font-medium">Labels</h3>
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ const LabelsSettings: NextPage = () => {
|
|||||||
Add label
|
Add label
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 py-6">
|
<div className="space-y-3 py-6 h-full w-full">
|
||||||
{labelForm && (
|
{labelForm && (
|
||||||
<CreateUpdateLabelInline
|
<CreateUpdateLabelInline
|
||||||
labelForm={labelForm}
|
labelForm={labelForm}
|
||||||
|
@ -334,7 +334,7 @@ const MembersSettings: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
|
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
|
||||||
<h4 className="text-xl font-medium border-b border-custom-border-100">Members</h4>
|
<h4 className="text-xl font-medium">Members</h4>
|
||||||
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
|
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
{!projectMembers || !projectInvitations ? (
|
{!projectMembers || !projectInvitations ? (
|
||||||
@ -386,11 +386,13 @@ const MembersSettings: NextPage = () => {
|
|||||||
<h4 className="text-sm">{member.display_name || member.email}</h4>
|
<h4 className="text-sm">{member.display_name || member.email}</h4>
|
||||||
)}
|
)}
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<p className="mt-0.5 text-xs text-custom-text-200">{member.email}</p>
|
<p className="mt-0.5 text-xs text-custom-sidebar-text-300">
|
||||||
|
{member.email}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-3 text-xs">
|
||||||
{!member.member && (
|
{!member.member && (
|
||||||
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
|
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
|
||||||
Pending
|
Pending
|
||||||
|
@ -8,7 +8,8 @@ import useSWR from "swr";
|
|||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
import { SettingsHeader } from "components/workspace";
|
// component
|
||||||
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { SecondaryButton } from "components/ui";
|
import { SecondaryButton } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
@ -42,14 +43,17 @@ const BillingSettings: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2 h-full overflow-hidden">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<section className="space-y-8">
|
<SettingsSidebar />
|
||||||
|
</div>
|
||||||
|
<section className="pr-9 py-8 w-full">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-semibold leading-6">Billing & Plans</h3>
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
<p className="mt-4 text-sm text-custom-text-200">Free launch preview</p>
|
<h3 className="text-xl font-medium">Billing & Plan</h3>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-8 md:w-2/3">
|
<div className="px-4 py-6">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-md mb-1 leading-6">Current plan</h4>
|
<h4 className="text-md mb-1 leading-6">Current plan</h4>
|
||||||
<p className="mb-3 text-sm text-custom-text-200">
|
<p className="mb-3 text-sm text-custom-text-200">
|
||||||
|
@ -6,10 +6,9 @@ import useSWR from "swr";
|
|||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
import { SettingsHeader } from "components/workspace";
|
|
||||||
// components
|
// components
|
||||||
import ExportGuide from "components/exporter/guide";
|
import ExportGuide from "components/exporter/guide";
|
||||||
import { IntegrationAndImportExportBanner } from "components/ui";
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// types
|
// types
|
||||||
@ -41,10 +40,16 @@ const ImportExport: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-8 space-y-4">
|
<div className="flex flex-row gap-2">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<IntegrationAndImportExportBanner bannerName="Export" />
|
<SettingsSidebar />
|
||||||
<ExportGuide />
|
</div>
|
||||||
|
<div className="pr-9 py-8 overflow-y-auto w-full">
|
||||||
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
|
<h3 className="text-xl font-medium">Exports</h3>
|
||||||
|
</div>
|
||||||
|
<ExportGuide />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</WorkspaceAuthorizationLayout>
|
</WorkspaceAuthorizationLayout>
|
||||||
);
|
);
|
||||||
|
@ -6,10 +6,9 @@ import useSWR from "swr";
|
|||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
import { SettingsHeader } from "components/workspace";
|
|
||||||
// components
|
// components
|
||||||
import IntegrationGuide from "components/integration/guide";
|
import IntegrationGuide from "components/integration/guide";
|
||||||
import { IntegrationAndImportExportBanner } from "components/ui";
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// types
|
// types
|
||||||
@ -41,15 +40,19 @@ const ImportExport: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-8 space-y-4">
|
<div className="flex flex-row gap-2 h-full overflow-hidden">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<IntegrationAndImportExportBanner
|
<SettingsSidebar />
|
||||||
bannerName="Import/ Export"
|
</div>
|
||||||
description="Integrations and importers are only available on the cloud version. We plan to open-source
|
<section className="pr-9 py-8 w-full">
|
||||||
our SDKs in the near future so that the community can request or contribute integrations as
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
needed."
|
<h3 className="text-xl font-medium">Imports</h3>
|
||||||
/>
|
</div>
|
||||||
<IntegrationGuide />
|
<IntegrationGuide />
|
||||||
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
|
<h3 className="text-xl font-medium">Previous Imports</h3>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</WorkspaceAuthorizationLayout>
|
</WorkspaceAuthorizationLayout>
|
||||||
);
|
);
|
||||||
|
@ -16,14 +16,16 @@ import useUserAuth from "hooks/use-user-auth";
|
|||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
// components
|
// components
|
||||||
import { ImageUploadModal } from "components/core";
|
import { ImageUploadModal } from "components/core";
|
||||||
import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace";
|
import { DeleteWorkspaceModal } from "components/workspace";
|
||||||
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, Input, CustomSelect, SecondaryButton, DangerButton } from "components/ui";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
|
import { Spinner, Input, CustomSelect, DangerButton, PrimaryButton, Icon } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { LinkIcon } from "@heroicons/react/24/outline";
|
import { Pencil } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import type { IWorkspace } from "types";
|
import type { IWorkspace } from "types";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
@ -135,6 +137,7 @@ const WorkspaceSettings: NextPage = () => {
|
|||||||
logo: "",
|
logo: "",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
setIsImageUploadModalOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -162,6 +165,8 @@ const WorkspaceSettings: NextPage = () => {
|
|||||||
<ImageUploadModal
|
<ImageUploadModal
|
||||||
isOpen={isImageUploadModalOpen}
|
isOpen={isImageUploadModalOpen}
|
||||||
onClose={() => setIsImageUploadModalOpen(false)}
|
onClose={() => setIsImageUploadModalOpen(false)}
|
||||||
|
isRemoving={isImageRemoving}
|
||||||
|
handleDelete={() => handleDelete(activeWorkspace?.logo)}
|
||||||
onSuccess={(imageUrl) => {
|
onSuccess={(imageUrl) => {
|
||||||
setIsImageUploading(true);
|
setIsImageUploading(true);
|
||||||
setValue("logo", imageUrl);
|
setValue("logo", imageUrl);
|
||||||
@ -178,67 +183,109 @@ const WorkspaceSettings: NextPage = () => {
|
|||||||
data={activeWorkspace ?? null}
|
data={activeWorkspace ?? null}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2 h-full w-full">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
|
<SettingsSidebar />
|
||||||
|
</div>
|
||||||
{activeWorkspace ? (
|
{activeWorkspace ? (
|
||||||
<div className={`space-y-8 sm:space-y-12 ${isAdmin ? "" : "opacity-60"}`}>
|
<div className={`pr-9 py-8 w-full ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
<div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-lg font-semibold">Logo</h4>
|
<button
|
||||||
<p className="text-sm text-custom-text-200">
|
type="button"
|
||||||
Max file size is 5MB. Supported file types are .jpg and .png.
|
onClick={() => setIsImageUploadModalOpen(true)}
|
||||||
</p>
|
disabled={!isAdmin}
|
||||||
|
>
|
||||||
|
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||||
|
<div className="relative mx-auto flex h-14 w-14">
|
||||||
|
<img
|
||||||
|
src={watch("logo")!}
|
||||||
|
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
||||||
|
alt="Workspace Logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative flex h-14 w-14 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
||||||
|
{activeWorkspace?.name?.charAt(0) ?? "N"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-4">
|
<h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
|
||||||
|
<span className="text-sm tracking-tight">{`${
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
window.location.origin.replace("http://", "").replace("https://", "")
|
||||||
|
}/${activeWorkspace.slug}`}</span>
|
||||||
|
<div className="flex item-center gap-2.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
|
||||||
onClick={() => setIsImageUploadModalOpen(true)}
|
onClick={() => setIsImageUploadModalOpen(true)}
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
>
|
>
|
||||||
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||||
<div className="relative mx-auto flex h-12 w-12">
|
<>
|
||||||
<img
|
<Pencil className="h-3 w-3" />
|
||||||
src={watch("logo")!}
|
Edit logo
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
</>
|
||||||
alt="Workspace Logo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="relative flex h-12 w-12 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
"Upload logo"
|
||||||
{activeWorkspace?.name?.charAt(0) ?? "N"}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{isAdmin && (
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<SecondaryButton
|
|
||||||
onClick={() => {
|
|
||||||
setIsImageUploadModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isImageUploading ? "Uploading..." : "Upload"}
|
|
||||||
</SecondaryButton>
|
|
||||||
{activeWorkspace.logo && activeWorkspace.logo !== "" && (
|
|
||||||
<DangerButton
|
|
||||||
onClick={() => handleDelete(activeWorkspace.logo)}
|
|
||||||
loading={isImageRemoving}
|
|
||||||
>
|
|
||||||
{isImageRemoving ? "Removing..." : "Remove"}
|
|
||||||
</DangerButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="flex flex-col gap-8 my-10">
|
||||||
<h4 className="text-lg font-semibold">URL</h4>
|
<div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
|
||||||
<p className="text-sm text-custom-text-200">Your workspace URL.</p>
|
<div className="flex flex-col gap-1 ">
|
||||||
</div>
|
<h4 className="text-sm">Workspace Name</h4>
|
||||||
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
|
<Input
|
||||||
<div className="flex flex-col gap-1">
|
id="name"
|
||||||
|
name="name"
|
||||||
|
placeholder="Name"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
error={errors.name}
|
||||||
|
validations={{
|
||||||
|
required: "Name is required",
|
||||||
|
maxLength: {
|
||||||
|
value: 80,
|
||||||
|
message: "Workspace name should not exceed 80 characters",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
disabled={!isAdmin}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1 ">
|
||||||
|
<h4 className="text-sm">Company Size</h4>
|
||||||
|
<Controller
|
||||||
|
name="organization_size"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
label={
|
||||||
|
ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"
|
||||||
|
}
|
||||||
|
width="w-full"
|
||||||
|
input
|
||||||
|
disabled={!isAdmin}
|
||||||
|
>
|
||||||
|
{ORGANIZATION_SIZE?.map((item) => (
|
||||||
|
<CustomSelect.Option key={item} value={item}>
|
||||||
|
{item}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1 ">
|
||||||
|
<h4 className="text-sm">Workspace URL</h4>
|
||||||
<Input
|
<Input
|
||||||
id="url"
|
id="url"
|
||||||
name="url"
|
name="url"
|
||||||
@ -253,114 +300,66 @@ const WorkspaceSettings: NextPage = () => {
|
|||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton
|
|
||||||
className="h-min"
|
|
||||||
onClick={() =>
|
|
||||||
copyTextToClipboard(
|
|
||||||
`${typeof window !== "undefined" && window.location.origin}/${
|
|
||||||
activeWorkspace.slug
|
|
||||||
}`
|
|
||||||
).then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link Copied!",
|
|
||||||
message: "Workspace link copied to clipboard.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
outline
|
|
||||||
>
|
|
||||||
<LinkIcon className="h-[18px] w-[18px]" />
|
|
||||||
</SecondaryButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
<div className="flex items-center justify-between py-2">
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<PrimaryButton
|
||||||
<h4 className="text-lg font-semibold">Name</h4>
|
onClick={handleSubmit(onSubmit)}
|
||||||
<p className="text-sm text-custom-text-200">Give a name to your workspace.</p>
|
loading={isSubmitting}
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
placeholder="Name"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
error={errors.name}
|
|
||||||
validations={{
|
|
||||||
required: "Name is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 80,
|
|
||||||
message: "Workspace name should not exceed 80 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
/>
|
>
|
||||||
</div>
|
{isSubmitting ? "Updating..." : "Update Workspace"}
|
||||||
</div>
|
</PrimaryButton>
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold">Organization Size</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">What size is your organization?</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<Controller
|
|
||||||
name="organization_size"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<CustomSelect
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
label={
|
|
||||||
ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"
|
|
||||||
}
|
|
||||||
width="w-full"
|
|
||||||
input
|
|
||||||
disabled={!isAdmin}
|
|
||||||
>
|
|
||||||
{ORGANIZATION_SIZE?.map((item) => (
|
|
||||||
<CustomSelect.Option key={item} value={item}>
|
|
||||||
{item}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAdmin && (
|
<Disclosure as="div" className="border-t border-custom-border-400">
|
||||||
<>
|
{({ open }) => (
|
||||||
<div className="sm:text-right">
|
<div className="w-full">
|
||||||
<SecondaryButton
|
<Disclosure.Button
|
||||||
onClick={handleSubmit(onSubmit)}
|
as="button"
|
||||||
loading={isSubmitting}
|
type="button"
|
||||||
disabled={!isAdmin}
|
className="flex items-center justify-between w-full py-4"
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Updating..." : "Update Workspace"}
|
<span className="text-xl tracking-tight">Delete Workspace</span>
|
||||||
</SecondaryButton>
|
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
|
||||||
|
</Disclosure.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
enter="transition duration-100 ease-out"
|
||||||
|
enterFrom="transform opacity-0"
|
||||||
|
enterTo="transform opacity-100"
|
||||||
|
leave="transition duration-75 ease-out"
|
||||||
|
leaveFrom="transform opacity-100"
|
||||||
|
leaveTo="transform opacity-0"
|
||||||
|
>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<span className="text-sm tracking-tight">
|
||||||
|
The danger zone of the project delete page is a critical area that
|
||||||
|
requires careful consideration and attention. When deleting a project, all
|
||||||
|
of the data and resources within that project will be permanently removed
|
||||||
|
and cannot be recovered.
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<DangerButton
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="!text-sm"
|
||||||
|
outline
|
||||||
|
>
|
||||||
|
Delete my project
|
||||||
|
</DangerButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
)}
|
||||||
<div className="col-span-12 sm:col-span-6">
|
</Disclosure>
|
||||||
<h4 className="text-lg font-semibold">Danger Zone</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
The danger zone of the workspace delete page is a critical area that requires
|
|
||||||
careful consideration and attention. When deleting a workspace, all of the
|
|
||||||
data and resources within that workspace will be permanently removed and
|
|
||||||
cannot be recovered.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<DangerButton onClick={() => setIsOpen(true)} outline>
|
|
||||||
Delete the workspace
|
|
||||||
</DangerButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
<div className="flex items-center justify-center h-full w-full px-4 sm:px-0">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -9,9 +9,9 @@ import workspaceService from "services/workspace.service";
|
|||||||
import IntegrationService from "services/integration";
|
import IntegrationService from "services/integration";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
import { SettingsHeader } from "components/workspace";
|
|
||||||
// components
|
// components
|
||||||
import { SingleIntegrationCard } from "components/integration";
|
import { SingleIntegrationCard } from "components/integration";
|
||||||
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { IntegrationAndImportExportBanner, Loader } from "components/ui";
|
import { IntegrationAndImportExportBanner, Loader } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
@ -48,19 +48,21 @@ const WorkspaceIntegrations: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<section className="space-y-5">
|
<SettingsSidebar />
|
||||||
|
</div>
|
||||||
|
<section className="pr-9 py-8 w-full">
|
||||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||||
<div className="space-y-5">
|
<div>
|
||||||
{appIntegrations ? (
|
{appIntegrations ? (
|
||||||
appIntegrations.map((integration) => (
|
appIntegrations.map((integration) => (
|
||||||
<SingleIntegrationCard key={integration.id} integration={integration} />
|
<SingleIntegrationCard key={integration.id} integration={integration} />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-5">
|
<Loader className="space-y-1">
|
||||||
<Loader.Item height="60px" />
|
<Loader.Item height="89px" />
|
||||||
<Loader.Item height="60px" />
|
<Loader.Item height="89px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,15 +13,15 @@ import useUser from "hooks/use-user";
|
|||||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
import { SettingsHeader } from "components/workspace";
|
|
||||||
// components
|
// components
|
||||||
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
|
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
|
||||||
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
|
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
|
||||||
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, CustomSelect, Loader } from "components/ui";
|
import { CustomMenu, CustomSelect, Icon, Loader, PrimaryButton } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { XMarkIcon } from "components/icons";
|
||||||
// types
|
// types
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -143,9 +143,8 @@ const MembersSettings: NextPage = () => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
mutateMembers(
|
mutateMembers((prevData: any) =>
|
||||||
(prevData: any) =>
|
prevData?.filter((item: any) => item.id !== selectedRemoveMember)
|
||||||
prevData?.filter((item: any) => item.id !== selectedRemoveMember)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -187,19 +186,14 @@ const MembersSettings: NextPage = () => {
|
|||||||
user={user}
|
user={user}
|
||||||
onSuccess={handleInviteModalSuccess}
|
onSuccess={handleInviteModalSuccess}
|
||||||
/>
|
/>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<section className="space-y-5">
|
<SettingsSidebar />
|
||||||
<div className="flex items-end justify-between gap-4">
|
</div>
|
||||||
<h3 className="text-2xl font-semibold">Members</h3>
|
<section className="pr-9 py-8 w-full">
|
||||||
<button
|
<div className="flex items-center justify-between gap-4 pt-2 pb-3.5 border-b border-custom-border-200">
|
||||||
type="button"
|
<h4 className="text-xl font-medium">Members</h4>
|
||||||
className="flex items-center gap-2 text-custom-primary outline-none"
|
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
|
||||||
onClick={() => setInviteModal(true)}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Member
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{!workspaceMembers || !workspaceInvitations ? (
|
{!workspaceMembers || !workspaceInvitations ? (
|
||||||
<Loader className="space-y-5">
|
<Loader className="space-y-5">
|
||||||
@ -209,23 +203,30 @@ const MembersSettings: NextPage = () => {
|
|||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200 bg-custom-background-100 px-6">
|
<div className="divide-y divide-custom-border-200">
|
||||||
{members.length > 0
|
{members.length > 0
|
||||||
? members.map((member) => (
|
? members.map((member) => (
|
||||||
<div key={member.id} className="flex items-center justify-between py-6">
|
<div
|
||||||
|
key={member.id}
|
||||||
|
className="group flex items-center justify-between px-3.5 py-[18px]"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-x-8 gap-y-2">
|
<div className="flex items-center gap-x-8 gap-y-2">
|
||||||
{member.avatar && member.avatar !== "" ? (
|
{member.avatar && member.avatar !== "" ? (
|
||||||
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
|
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||||
<img
|
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
|
||||||
src={member.avatar}
|
<img
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
|
src={member.avatar}
|
||||||
alt={member.display_name || member.email}
|
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
|
||||||
/>
|
alt={member.display_name || member.email}
|
||||||
</div>
|
/>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
) : member.display_name || member.email ? (
|
) : member.display_name || member.email ? (
|
||||||
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||||
{(member.display_name || member.email)?.charAt(0)}
|
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
||||||
</div>
|
{(member.display_name || member.email)?.charAt(0)}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
||||||
?
|
?
|
||||||
@ -244,14 +245,18 @@ const MembersSettings: NextPage = () => {
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<h4 className="text-sm">{member.display_name || member.email}</h4>
|
<h4 className="text-sm cursor-default">
|
||||||
|
{member.display_name || member.email}
|
||||||
|
</h4>
|
||||||
)}
|
)}
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<p className="text-xs text-custom-text-200">{member.email}</p>
|
<p className="mt-0.5 text-xs text-custom-sidebar-text-300">
|
||||||
|
{member.email}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-3 text-xs">
|
||||||
{!member?.status && (
|
{!member?.status && (
|
||||||
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
|
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
|
||||||
<p>Pending</p>
|
<p>Pending</p>
|
||||||
@ -263,9 +268,22 @@ const MembersSettings: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
label={ROLE[member.role as keyof typeof ROLE]}
|
customButton={
|
||||||
|
<button className="flex item-center gap-1">
|
||||||
|
<span
|
||||||
|
className={`flex items-center text-sm font-medium ${
|
||||||
|
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{ROLE[member.role as keyof typeof ROLE]}
|
||||||
|
</span>
|
||||||
|
{member.memberId !== user?.id && (
|
||||||
|
<Icon iconName="expand_more" className="text-lg font-medium" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
value={member.role}
|
value={member.role}
|
||||||
onChange={(value: any) => {
|
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
mutateMembers(
|
mutateMembers(
|
||||||
@ -323,7 +341,14 @@ const MembersSettings: NextPage = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user?.id === member.memberId ? "Leave" : "Remove member"}
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<XMarkIcon className="h-4 w-4" />
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{" "}
|
||||||
|
{user?.id === member.memberId ? "Leave" : "Remove member"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,38 +1,22 @@
|
|||||||
// next imports
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Router from "next/router";
|
import Router from "next/router";
|
||||||
|
|
||||||
// themes
|
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
|
import NProgress from "nprogress";
|
||||||
// styles
|
// styles
|
||||||
import "styles/globals.css";
|
import "styles/globals.css";
|
||||||
import "styles/editor.css";
|
import "styles/editor.css";
|
||||||
import "styles/command-pallette.css";
|
import "styles/command-pallette.css";
|
||||||
import "styles/nprogress.css";
|
import "styles/nprogress.css";
|
||||||
import "styles/react-datepicker.css";
|
import "styles/react-datepicker.css";
|
||||||
|
|
||||||
// nprogress
|
|
||||||
import NProgress from "nprogress";
|
|
||||||
|
|
||||||
// contexts
|
// contexts
|
||||||
import { UserProvider } from "contexts/user.context";
|
|
||||||
import { ToastContextProvider } from "contexts/toast.context";
|
import { ToastContextProvider } from "contexts/toast.context";
|
||||||
import { ThemeContextProvider } from "contexts/theme.context";
|
|
||||||
// types
|
// types
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
// constants
|
// constants
|
||||||
import { THEMES } from "constants/themes";
|
import { THEMES } from "constants/themes";
|
||||||
// constants
|
// constants
|
||||||
import {
|
import { SITE_TITLE } from "constants/seo-variables";
|
||||||
SITE_NAME,
|
|
||||||
SITE_DESCRIPTION,
|
|
||||||
SITE_URL,
|
|
||||||
TWITTER_USER_NAME,
|
|
||||||
SITE_KEYWORDS,
|
|
||||||
SITE_TITLE,
|
|
||||||
} from "constants/seo-variables";
|
|
||||||
// mobx store provider
|
// mobx store provider
|
||||||
import { MobxStoreProvider } from "lib/mobx/store-provider";
|
import { MobxStoreProvider } from "lib/mobx/store-provider";
|
||||||
import MobxStoreInit from "lib/mobx/store-init";
|
import MobxStoreInit from "lib/mobx/store-init";
|
||||||
@ -47,33 +31,20 @@ Router.events.on("routeChangeComplete", NProgress.done);
|
|||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
// <UserProvider>
|
<>
|
||||||
// mobx root provider
|
<Head>
|
||||||
<MobxStoreProvider {...pageProps}>
|
<title>{SITE_TITLE}</title>
|
||||||
<ThemeProvider themes={THEMES} defaultTheme="system">
|
</Head>
|
||||||
<ToastContextProvider>
|
<MobxStoreProvider {...pageProps}>
|
||||||
<CrispWithNoSSR />
|
<ThemeProvider themes={THEMES} defaultTheme="system">
|
||||||
<Head>
|
<ToastContextProvider>
|
||||||
<title>{SITE_TITLE}</title>
|
<CrispWithNoSSR />
|
||||||
<meta property="og:site_name" content={SITE_NAME} />
|
<MobxStoreInit />
|
||||||
<meta property="og:title" content={SITE_TITLE} />
|
<Component {...pageProps} />
|
||||||
<meta property="og:url" content={SITE_URL} />
|
</ToastContextProvider>
|
||||||
<meta name="description" content={SITE_DESCRIPTION} />
|
</ThemeProvider>
|
||||||
<meta property="og:description" content={SITE_DESCRIPTION} />
|
</MobxStoreProvider>
|
||||||
<meta name="keywords" content={SITE_KEYWORDS} />
|
</>
|
||||||
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest.json" />
|
|
||||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<MobxStoreInit />
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</ToastContextProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</MobxStoreProvider>
|
|
||||||
// </UserProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,14 @@
|
|||||||
import Document, { Html, Head, Main, NextScript } from "next/document";
|
import Document, { Html, Head, Main, NextScript } from "next/document";
|
||||||
|
// constants
|
||||||
|
import {
|
||||||
|
SITE_NAME,
|
||||||
|
SITE_DESCRIPTION,
|
||||||
|
SITE_URL,
|
||||||
|
TWITTER_USER_NAME,
|
||||||
|
SITE_KEYWORDS,
|
||||||
|
SITE_TITLE,
|
||||||
|
} from "constants/seo-variables";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
class MyDocument extends Document {
|
class MyDocument extends Document {
|
||||||
render() {
|
render() {
|
||||||
@ -9,9 +19,32 @@ class MyDocument extends Document {
|
|||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<meta property="og:site_name" content={SITE_NAME} />
|
||||||
<link rel="apple-touch-icon" href="/icon.png" />
|
<meta property="og:title" content={SITE_TITLE} />
|
||||||
|
<meta property="og:url" content={SITE_URL} />
|
||||||
|
<meta name="description" content={SITE_DESCRIPTION} />
|
||||||
|
<meta property="og:description" content={SITE_DESCRIPTION} />
|
||||||
|
<meta name="keywords" content={SITE_KEYWORDS} />
|
||||||
|
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
|
||||||
<meta name="theme-color" content="#fff" />
|
<meta name="theme-color" content="#fff" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest.json" />
|
||||||
|
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||||
|
{isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && (
|
||||||
|
<Script id="clarity-tracking">
|
||||||
|
{`(function(c,l,a,r,i,t,y){
|
||||||
|
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||||
|
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||||
|
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||||
|
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY}");`}
|
||||||
|
</Script>
|
||||||
|
)}
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
|
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
@ -19,23 +52,12 @@ class MyDocument extends Document {
|
|||||||
src="https://plausible.io/js/script.js"
|
src="https://plausible.io/js/script.js"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2" />
|
{process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST && (
|
||||||
{isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && (
|
<Script id="posthog-tracking">
|
||||||
<script
|
{`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
||||||
defer
|
posthog.init('${process.env.NEXT_PUBLIC_POSTHOG_KEY}',{api_host:'${process.env.NEXT_PUBLIC_POSTHOG_HOST}'})`}
|
||||||
dangerouslySetInnerHTML={{
|
</Script>
|
||||||
__html: `(function(c,l,a,r,i,t,y){
|
|
||||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
|
||||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
|
||||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
|
||||||
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY}");`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Head>
|
|
||||||
<body>
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</Html>
|
||||||
);
|
);
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
// pages/api/slack/authorize.js
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
export default async function handleSlackAuthorize(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handleSlackAuthorize(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { code } = req.body;
|
try {
|
||||||
|
const { code } = req.body;
|
||||||
|
|
||||||
if (!code || code === "") return res.status(400).json({ message: "Code is required" });
|
if (!code || code === "") return res.status(400).json({ message: "Code is required" });
|
||||||
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: "https://slack.com/api/oauth.v2.access",
|
url: process.env.SLACK_OAUTH_URL || "",
|
||||||
params: {
|
params: {
|
||||||
client_id: process.env.NEXT_PUBLIC_SLACK_CLIENT_ID,
|
client_id: process.env.SLACK_CLIENT_ID,
|
||||||
client_secret: process.env.NEXT_PUBLIC_SLACK_CLIENT_SECRET,
|
client_secret: process.env.SLACK_CLIENT_SECRET,
|
||||||
code,
|
code,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
res.status(200).json(response?.data);
|
||||||
// if (response?.data?.ok)
|
} catch (error) {
|
||||||
res.status(200).json(response.data);
|
res.status(200).json({ message: "Internal Server Error" });
|
||||||
// else res.status(404).json(response.data);
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
// jitsu
|
// jitsu
|
||||||
import { createClient } from "@jitsu/nextjs";
|
import { createClient } from "@jitsu/nextjs";
|
||||||
import { convertCookieStringToObject } from "lib/cookie";
|
|
||||||
|
|
||||||
const jitsu = createClient({
|
const jitsuClient = createClient({
|
||||||
key: process.env.TRACKER_ACCESS_KEY || "",
|
key: process.env.JITSU_TRACKER_ACCESS_KEY || "",
|
||||||
tracking_host: "https://t.jitsu.com",
|
tracking_host: process.env.JITSU_TRACKER_HOST || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@ -18,18 +16,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
if (!user) return res.status(401).json({ message: "Unauthorized" });
|
if (!user) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
// TODO: cache user info
|
jitsuClient
|
||||||
|
|
||||||
jitsu
|
|
||||||
.id({
|
.id({
|
||||||
id: user.id,
|
id: user?.id,
|
||||||
email: user.email,
|
email: user?.email,
|
||||||
first_name: user.first_name,
|
first_name: user?.first_name,
|
||||||
last_name: user.last_name,
|
last_name: user?.last_name,
|
||||||
display_name: user?.display_name,
|
display_name: user?.display_name,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
jitsu.track(eventName, {
|
jitsuClient.track(eventName, {
|
||||||
...extra,
|
...extra,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import axios from "axios";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
// TODO: remove NEXT_PUBLIC_ prefix from env variable
|
const unsplashKey = process.env.UNSPLASH_ACCESS_KEY;
|
||||||
const unsplashKey = process.env.NEXT_PUBLIC_UNSPLASH_ACCESS;
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { query, page, per_page = 20 } = req.query;
|
const { query, page, per_page = 20 } = req.query;
|
||||||
@ -10,14 +10,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
? `https://api.unsplash.com/search/photos/?client_id=${unsplashKey}&query=${query}&page=${page}&per_page=${per_page}`
|
? `https://api.unsplash.com/search/photos/?client_id=${unsplashKey}&query=${query}&page=${page}&per_page=${per_page}`
|
||||||
: `https://api.unsplash.com/photos/?client_id=${unsplashKey}&page=${page}&per_page=${per_page}`;
|
: `https://api.unsplash.com/photos/?client_id=${unsplashKey}&page=${page}&per_page=${per_page}`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await axios({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
res.status(200).json(response);
|
||||||
|
|
||||||
res.status(200).json(data);
|
|
||||||
}
|
}
|
||||||
|
54
web/public/theme-mode/custom-mode.svg
Normal file
54
web/public/theme-mode/custom-mode.svg
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<svg width="201" height="150" viewBox="0 0 201 150" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_2267_36125" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="101" height="150">
|
||||||
|
<rect width="101" height="150" fill="#D9D9D9"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_2267_36125)">
|
||||||
|
<path d="M1.25 4.00001C1.25 1.92894 2.92893 0.25 5 0.25H225C227.071 0.25 228.75 1.92893 228.75 4V189.75H1.25V4.00001Z" fill="#231035" stroke="#3F2B58" stroke-width="0.5"/>
|
||||||
|
<path d="M2 4C2 2.34315 3.34315 1 5 1H204C205.657 1 207 2.34315 207 4V10H2V4Z" fill="#32184C"/>
|
||||||
|
<line x1="51.25" y1="10" x2="51.25" y2="190" stroke="#3F2B58" stroke-width="0.5"/>
|
||||||
|
<line x1="1" y1="9.75" x2="211" y2="9.75002" stroke="#3F2B58" stroke-width="0.5"/>
|
||||||
|
<rect x="5" y="14" width="36" height="6" rx="1" fill="#572D81"/>
|
||||||
|
<rect x="5" y="26" width="39" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="5" y="35" width="31" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="5" y="53" width="26" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="5" y="80" width="34" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="5" y="44" width="35" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="5" y="62" width="29" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="5" y="71" width="38" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="43" y="14" width="6" height="6" rx="3" fill="#572D81"/>
|
||||||
|
<rect x="66" y="44" width="51" height="4" rx="1" fill="#572D81"/>
|
||||||
|
<rect x="66" y="53" width="132" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="66" y="60" width="97" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="75" y="76" width="52" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="75" y="87" width="78" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="75" y="98" width="71" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="75" y="109" width="58" height="4" rx="1" fill="#371B52"/>
|
||||||
|
<rect x="66" y="75" width="6" height="6" rx="3" fill="#401E60"/>
|
||||||
|
<rect x="66" y="86" width="6" height="6" rx="3" fill="#401E60"/>
|
||||||
|
<rect x="66" y="97" width="6" height="6" rx="3" fill="#401E60"/>
|
||||||
|
<rect x="66" y="108" width="6" height="6" rx="3" fill="#401E60"/>
|
||||||
|
<rect x="66" y="26" width="12" height="12" rx="6" fill="#572D81"/>
|
||||||
|
<rect x="5" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
|
||||||
|
<rect x="9" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
|
||||||
|
<rect x="13" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
|
||||||
|
</g>
|
||||||
|
<mask id="mask1_2267_36125" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="101" y="0" width="100" height="150">
|
||||||
|
<rect x="101" width="100" height="150" fill="#D9D9D9"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask1_2267_36125)">
|
||||||
|
<path d="M0.5 4.00001C0.5 2.06701 2.067 0.5 4 0.5H224C225.933 0.5 227.5 2.067 227.5 4V189.5H0.5V4.00001Z" fill="#000C1B" stroke="#172534"/>
|
||||||
|
<path d="M1 4C1 2.34315 2.34315 1 4 1H203C204.657 1 206 2.34315 206 4V10H1V4Z" fill="#001936"/>
|
||||||
|
<line x1="2.18557e-08" y1="9.75" x2="210" y2="9.75002" stroke="#172534" stroke-width="0.5"/>
|
||||||
|
<rect x="65" y="44" width="51" height="4" rx="1" fill="#172B52"/>
|
||||||
|
<rect x="65" y="53" width="132" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
<rect x="65" y="60" width="97" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
<rect x="74" y="76" width="52" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
<rect x="74" y="87" width="78" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
<rect x="74" y="98" width="71" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
<rect x="74" y="109" width="58" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
<rect x="129" y="76" width="6" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
<rect x="155" y="87" width="6" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
<rect x="148" y="98" width="6" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
<rect x="135" y="109" width="6" height="4" rx="1" fill="#151E3D"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
33
web/public/theme-mode/custom-theme-banner.svg
Normal file
33
web/public/theme-mode/custom-theme-banner.svg
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<svg width="462" height="536" viewBox="0 0 462 536" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.827922 13.2468C0.827922 6.38804 6.38802 0.827922 13.2468 0.827922H741.818C748.677 0.827922 754.237 6.38802 754.237 13.2468V628.393H0.827922V13.2468Z" fill="white" stroke="#E5E5E5" stroke-width="1.65584"/>
|
||||||
|
<path d="M0.827922 13.2468C0.827922 6.38802 6.38802 0.827922 13.2468 0.827922H672.273C677.302 0.827922 681.38 4.90533 681.38 9.93507V32.289H0.827922V13.2468Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="1.65584"/>
|
||||||
|
<line x1="166.414" y1="33.1162" x2="166.414" y2="629.22" stroke="#E5E5E5" stroke-width="1.65584"/>
|
||||||
|
<line x1="-7.23793e-08" y1="32.2883" x2="695.455" y2="32.2882" stroke="#E5E5E5" stroke-width="1.65584"/>
|
||||||
|
<rect x="13.25" y="46.3633" width="119.221" height="19.8701" rx="3.31169" fill="#E5E5E5"/>
|
||||||
|
<rect x="13.25" y="86.1035" width="129.156" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="13.25" y="115.908" width="102.662" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="13.25" y="175.519" width="86.1039" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="13.25" y="264.935" width="112.597" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="13.25" y="145.714" width="115.909" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="13.25" y="205.324" width="96.039" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="13.25" y="235.129" width="125.844" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="139.094" y="46.3633" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
|
||||||
|
<rect x="215.266" y="145.715" width="168.896" height="13.2468" rx="3.31169" fill="#E5E5E5"/>
|
||||||
|
<rect x="397.406" y="145.715" width="43.0519" height="13.2468" rx="3.31169" fill="#3F76FF"/>
|
||||||
|
<rect x="215.266" y="175.521" width="437.143" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="215.266" y="198.702" width="321.234" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="245.07" y="251.688" width="172.208" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="245.07" y="288.117" width="258.312" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="245.07" y="324.546" width="235.13" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="245.07" y="360.975" width="192.078" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="427.219" y="251.688" width="19.8701" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="447.078" y="360.975" width="19.8701" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
|
||||||
|
<rect x="215.266" y="248.378" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
|
||||||
|
<rect x="215.266" y="284.806" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
|
||||||
|
<rect x="215.266" y="321.234" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
|
||||||
|
<rect x="215.266" y="357.663" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
|
||||||
|
<rect x="215.266" y="86.1045" width="39.7403" height="39.7403" rx="19.8701" fill="#D4D4D4"/>
|
||||||
|
<rect x="13.2422" y="13.2471" width="9.93506" height="9.93506" rx="4.96753" fill="#EF4444"/>
|
||||||
|
<rect x="26.4922" y="13.2471" width="9.93506" height="9.93506" rx="4.96753" fill="#FCD34D"/>
|
||||||
|
<rect x="39.7422" y="13.2471" width="9.93506" height="9.93506" rx="4.96753" fill="#4ADE80"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
34
web/public/theme-mode/dark-high-contrast.svg
Normal file
34
web/public/theme-mode/dark-high-contrast.svg
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<svg width="201" height="151" viewBox="0 0 201 151" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="#171717" stroke="white" stroke-width="0.5"/>
|
||||||
|
<path d="M1 4C1 2.34315 2.34315 1 4 1H203C204.657 1 206 2.34315 206 4V10H1V4Z" fill="#171717"/>
|
||||||
|
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="white" stroke-width="0.5"/>
|
||||||
|
<line x1="2.18557e-08" y1="9.75" x2="210" y2="9.75002" stroke="white" stroke-width="0.5"/>
|
||||||
|
<rect x="4" y="14" width="36" height="6" rx="1" fill="white"/>
|
||||||
|
<rect x="4" y="26" width="39" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="4" y="35" width="31" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="4" y="53" width="26" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="4" y="80" width="34" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="4" y="44" width="35" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="4" y="62" width="29" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="4" y="71" width="38" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="42" y="14" width="6" height="6" rx="3" fill="white"/>
|
||||||
|
<rect x="65" y="44" width="51" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="65" y="53" width="132" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="65" y="60" width="97" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="74" y="76" width="52" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="74" y="87" width="78" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="74" y="98" width="71" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="74" y="109" width="58" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="129" y="76" width="6" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="155" y="87" width="6" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="148" y="98" width="6" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="135" y="109" width="6" height="4" rx="1" fill="#D4D4D4"/>
|
||||||
|
<rect x="65" y="75" width="6" height="6" rx="3" fill="white"/>
|
||||||
|
<rect x="65" y="86" width="6" height="6" rx="3" fill="white"/>
|
||||||
|
<rect x="65" y="97" width="6" height="6" rx="3" fill="white"/>
|
||||||
|
<rect x="65" y="108" width="6" height="6" rx="3" fill="white"/>
|
||||||
|
<rect x="65" y="26" width="12" height="12" rx="6" fill="#F1F1F1"/>
|
||||||
|
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
|
||||||
|
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
|
||||||
|
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
41
web/public/theme-mode/dark-mode.svg
Normal file
41
web/public/theme-mode/dark-mode.svg
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<svg width="201" height="154" viewBox="0 0 201 154" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_2273_36428)">
|
||||||
|
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="#121212" stroke="#262626" stroke-width="0.5"/>
|
||||||
|
<path d="M0.25 4C0.25 1.92893 1.92893 0.25 4 0.25H203C204.519 0.25 205.75 1.48122 205.75 3V9.75H0.25V4Z" fill="#222222" stroke="#222222" stroke-width="0.5"/>
|
||||||
|
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="#262626" stroke-width="0.5"/>
|
||||||
|
<rect x="4" y="14" width="36" height="6" rx="1" fill="#404040"/>
|
||||||
|
<rect x="4" y="26" width="39" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="4" y="35" width="31" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="4" y="53" width="26" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="4" y="80" width="34" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="4" y="44" width="35" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="4" y="62" width="29" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="4" y="71" width="38" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="42" y="14" width="6" height="6" rx="3" fill="#404040"/>
|
||||||
|
<rect x="65" y="44" width="51" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="65" y="53" width="132" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="65" y="60" width="97" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="74" y="76" width="52" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="74" y="87" width="78" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="74" y="98" width="71" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="74" y="109" width="58" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="129" y="76" width="6" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="155" y="87" width="6" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="148" y="98" width="6" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="135" y="109" width="6" height="4" rx="1" fill="#262626"/>
|
||||||
|
<rect x="65" y="75" width="6" height="6" rx="3" fill="#222222"/>
|
||||||
|
<rect x="65" y="86" width="6" height="6" rx="3" fill="#222222"/>
|
||||||
|
<rect x="65" y="97" width="6" height="6" rx="3" fill="#222222"/>
|
||||||
|
<rect x="65" y="108" width="6" height="6" rx="3" fill="#222222"/>
|
||||||
|
<rect x="65" y="26" width="12" height="12" rx="6" fill="#404040"/>
|
||||||
|
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
|
||||||
|
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
|
||||||
|
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
|
||||||
|
<rect x="121" y="44" width="13" height="4" rx="1" fill="#3F76FF"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2273_36428">
|
||||||
|
<rect width="201" height="154" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
42
web/public/theme-mode/light-high-contrast.svg
Normal file
42
web/public/theme-mode/light-high-contrast.svg
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<svg width="204" height="154" viewBox="0 0 204 154" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_2273_36429)">
|
||||||
|
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="white" stroke="black" stroke-width="0.5"/>
|
||||||
|
<path d="M1 4C1 2.34315 2.34315 1 4 1H203C204.657 1 206 2.34315 206 4V10H1V4Z" fill="white"/>
|
||||||
|
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="black" stroke-width="0.5"/>
|
||||||
|
<line x1="-2.18557e-08" y1="9.75" x2="210" y2="9.74998" stroke="black" stroke-width="0.5"/>
|
||||||
|
<rect x="4" y="14" width="36" height="6" rx="1" fill="#2E2E2E"/>
|
||||||
|
<rect x="4" y="26" width="39" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="4" y="35" width="31" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="4" y="53" width="26" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="4" y="80" width="34" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="4" y="44" width="35" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="4" y="62" width="29" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="4" y="71" width="38" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="42" y="14" width="6" height="6" rx="3" fill="#2E2E2E"/>
|
||||||
|
<rect x="65" y="44" width="51" height="4" rx="1" fill="#2E2E2E"/>
|
||||||
|
<rect x="65" y="53" width="132" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="65" y="60" width="97" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="74" y="76" width="52" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="74" y="87" width="78" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="74" y="98" width="71" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="74" y="109" width="58" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="129" y="76" width="6" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="155" y="87" width="6" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="148" y="98" width="6" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="135" y="109" width="6" height="4" rx="1" fill="#404040"/>
|
||||||
|
<rect x="65" y="75" width="6" height="6" rx="3" fill="#3A3A3A"/>
|
||||||
|
<rect x="65" y="86" width="6" height="6" rx="3" fill="#3A3A3A"/>
|
||||||
|
<rect x="65" y="97" width="6" height="6" rx="3" fill="#3A3A3A"/>
|
||||||
|
<rect x="65" y="108" width="6" height="6" rx="3" fill="#3A3A3A"/>
|
||||||
|
<rect x="65" y="26" width="12" height="12" rx="6" fill="#262626"/>
|
||||||
|
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
|
||||||
|
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
|
||||||
|
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
|
||||||
|
<rect x="121" y="44" width="13" height="4" rx="1" fill="#3F76FF"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2273_36429">
|
||||||
|
<rect width="204" height="154" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
42
web/public/theme-mode/light-mode.svg
Normal file
42
web/public/theme-mode/light-mode.svg
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<svg width="201" height="154" viewBox="0 0 201 154" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_2273_36427)">
|
||||||
|
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="white" stroke="#E5E5E5" stroke-width="0.5"/>
|
||||||
|
<path d="M0.25 4C0.25 1.92893 1.92893 0.25 4 0.25H203C204.519 0.25 205.75 1.48122 205.75 3V9.75H0.25V4Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="0.5"/>
|
||||||
|
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="#E5E5E5" stroke-width="0.5"/>
|
||||||
|
<line x1="-2.18557e-08" y1="9.75" x2="210" y2="9.74998" stroke="#E5E5E5" stroke-width="0.5"/>
|
||||||
|
<rect x="4" y="14" width="36" height="6" rx="1" fill="#E5E5E5"/>
|
||||||
|
<rect x="4" y="26" width="39" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="4" y="35" width="31" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="4" y="53" width="26" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="4" y="80" width="34" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="4" y="44" width="35" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="4" y="62" width="29" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="4" y="71" width="38" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="42" y="14" width="6" height="6" rx="3" fill="#E5E5E5"/>
|
||||||
|
<rect x="65" y="44" width="51" height="4" rx="1" fill="#E5E5E5"/>
|
||||||
|
<rect x="120" y="44" width="13" height="4" rx="1" fill="#3F76FF"/>
|
||||||
|
<rect x="65" y="53" width="132" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="65" y="60" width="97" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="74" y="76" width="52" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="74" y="87" width="78" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="74" y="98" width="71" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="74" y="109" width="58" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="129" y="76" width="6" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="155" y="87" width="6" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="148" y="98" width="6" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="135" y="109" width="6" height="4" rx="1" fill="#F1F1F1"/>
|
||||||
|
<rect x="65" y="75" width="6" height="6" rx="3" fill="#E5E5E5"/>
|
||||||
|
<rect x="65" y="86" width="6" height="6" rx="3" fill="#E5E5E5"/>
|
||||||
|
<rect x="65" y="97" width="6" height="6" rx="3" fill="#E5E5E5"/>
|
||||||
|
<rect x="65" y="108" width="6" height="6" rx="3" fill="#E5E5E5"/>
|
||||||
|
<rect x="65" y="26" width="12" height="12" rx="6" fill="#D4D4D4"/>
|
||||||
|
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
|
||||||
|
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
|
||||||
|
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2273_36427">
|
||||||
|
<rect width="201" height="154" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
@ -5,9 +5,6 @@ import { ICurrentUserResponse, IGptResponse } from "types";
|
|||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class AiServices extends APIService {
|
class AiServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -21,7 +18,7 @@ class AiServices extends APIService {
|
|||||||
): Promise<IGptResponse> {
|
): Promise<IGptResponse> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackAskGptEvent(response?.data, "ASK_GPT", user);
|
trackEventServices.trackAskGptEvent(response?.data, "ASK_GPT", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -5,9 +5,6 @@ import trackEventServices from "services/track-event.service";
|
|||||||
import type { CycleDateCheckData, ICurrentUserResponse, ICycle, IIssue } from "types";
|
import type { CycleDateCheckData, ICurrentUserResponse, ICycle, IIssue } from "types";
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class ProjectCycleServices extends APIService {
|
class ProjectCycleServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -21,7 +18,7 @@ class ProjectCycleServices extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user);
|
trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -99,7 +96,7 @@ class ProjectCycleServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
|
trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -119,7 +116,7 @@ class ProjectCycleServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
|
trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -135,7 +132,7 @@ class ProjectCycleServices extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`)
|
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_DELETE", user);
|
trackEventServices.trackCycleEvent(response?.data, "CYCLE_DELETE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -5,9 +5,6 @@ import type { ICurrentUserResponse, IEstimate, IEstimateFormData } from "types";
|
|||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class ProjectEstimateServices extends APIService {
|
class ProjectEstimateServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -21,8 +18,7 @@ class ProjectEstimateServices extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE", user);
|
||||||
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -42,8 +38,7 @@ class ProjectEstimateServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE", user);
|
||||||
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -83,8 +78,7 @@ class ProjectEstimateServices extends APIService {
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE", user);
|
||||||
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type {
|
import type {
|
||||||
IInboxIssue,
|
IInboxIssue,
|
||||||
@ -12,7 +8,6 @@ import type {
|
|||||||
TInboxStatus,
|
TInboxStatus,
|
||||||
IInboxIssueDetail,
|
IInboxIssueDetail,
|
||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
IInboxFilterOptions,
|
|
||||||
IInboxQueryParams,
|
IInboxQueryParams,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
@ -95,8 +90,7 @@ class InboxServices extends APIService {
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_DELETE", user);
|
||||||
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_DELETE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -125,7 +119,7 @@ class InboxServices extends APIService {
|
|||||||
: data.status === 1
|
: data.status === 1
|
||||||
? "INBOX_ISSUE_ACCEPTED"
|
? "INBOX_ISSUE_ACCEPTED"
|
||||||
: "INBOX_ISSUE_DUPLICATED";
|
: "INBOX_ISSUE_DUPLICATED";
|
||||||
if (trackEvent) trackEventServices.trackInboxEvent(response?.data, action, user);
|
trackEventServices.trackInboxEvent(response?.data, action, user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -146,8 +140,7 @@ class InboxServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_UPDATE", user);
|
||||||
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_UPDATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -167,8 +160,7 @@ class InboxServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE", user);
|
||||||
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -3,9 +3,6 @@ import trackEventServices from "services/track-event.service";
|
|||||||
import { ICurrentUserResponse } from "types";
|
import { ICurrentUserResponse } from "types";
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class CSVIntegrationService extends APIService {
|
class CSVIntegrationService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -21,14 +18,13 @@ class CSVIntegrationService extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/export-issues/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/export-issues/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackExporterEvent(
|
||||||
trackEventServices.trackExporterEvent(
|
{
|
||||||
{
|
workspaceSlug,
|
||||||
workspaceSlug,
|
},
|
||||||
},
|
"CSV_EXPORTER_CREATE",
|
||||||
"CSV_EXPORTER_CREATE",
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -4,11 +4,6 @@ import { API_BASE_URL } from "helpers/common.helper";
|
|||||||
|
|
||||||
import { ICurrentUserResponse, IGithubRepoInfo, IGithubServiceImportFormData } from "types";
|
import { ICurrentUserResponse, IGithubRepoInfo, IGithubServiceImportFormData } from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
const integrationServiceType: string = "github";
|
const integrationServiceType: string = "github";
|
||||||
class GithubIntegrationService extends APIService {
|
class GithubIntegrationService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -48,8 +43,7 @@ class GithubIntegrationService extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE", user);
|
||||||
trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -11,9 +11,6 @@ import {
|
|||||||
} from "types";
|
} from "types";
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class IntegrationService extends APIService {
|
class IntegrationService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -78,8 +75,7 @@ class IntegrationService extends APIService {
|
|||||||
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`)
|
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const eventName = service === "github" ? "GITHUB_IMPORTER_DELETE" : "JIRA_IMPORTER_DELETE";
|
const eventName = service === "github" ? "GITHUB_IMPORTER_DELETE" : "JIRA_IMPORTER_DELETE";
|
||||||
|
trackEventServices.trackImporterEvent(response?.data, eventName, user);
|
||||||
if (trackEvent) trackEventServices.trackImporterEvent(response?.data, eventName, user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -4,11 +4,6 @@ import { API_BASE_URL } from "helpers/common.helper";
|
|||||||
// types
|
// types
|
||||||
import { IJiraMetadata, IJiraResponse, IJiraImporterForm, ICurrentUserResponse } from "types";
|
import { IJiraMetadata, IJiraResponse, IJiraImporterForm, ICurrentUserResponse } from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class JiraImportedService extends APIService {
|
class JiraImportedService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -31,8 +26,7 @@ class JiraImportedService extends APIService {
|
|||||||
): Promise<IJiraResponse> {
|
): Promise<IJiraResponse> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE", user);
|
||||||
trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -12,9 +12,6 @@ import type {
|
|||||||
} from "types";
|
} from "types";
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class ProjectIssuesServices extends APIService {
|
class ProjectIssuesServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -28,7 +25,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
): Promise<IIssue> {
|
): Promise<IIssue> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_CREATE", user);
|
trackEventServices.trackIssueEvent(response.data, "ISSUE_CREATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -112,20 +109,19 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
|
||||||
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
|
{
|
||||||
{
|
workspaceSlug,
|
||||||
workspaceSlug,
|
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
|
||||||
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
|
projectId,
|
||||||
projectId,
|
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
|
||||||
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
|
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
|
||||||
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
|
issueId: response?.data?.[0]?.issue_detail?.id,
|
||||||
issueId: response?.data?.[0]?.issue_detail?.id,
|
cycleId,
|
||||||
cycleId,
|
},
|
||||||
},
|
response.data.length > 1 ? "ISSUE_MOVED_TO_CYCLE_IN_BULK" : "ISSUE_MOVED_TO_CYCLE",
|
||||||
response.data.length > 1 ? "ISSUE_MOVED_TO_CYCLE_IN_BULK" : "ISSUE_MOVED_TO_CYCLE",
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -165,8 +161,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueRelationEvent(response.data, "ISSUE_RELATION_CREATE", user);
|
||||||
trackEventServices.trackIssueRelationEvent(response.data, "ISSUE_RELATION_CREATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -185,8 +180,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/${relationId}/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/${relationId}/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueRelationEvent(response.data, "ISSUE_RELATION_DELETE", user);
|
||||||
trackEventServices.trackIssueRelationEvent(response.data, "ISSUE_RELATION_DELETE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -234,8 +228,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE", user);
|
||||||
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -256,8 +249,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE", user);
|
||||||
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -276,15 +268,14 @@ class ProjectIssuesServices extends APIService {
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueCommentEvent(
|
||||||
trackEventServices.trackIssueCommentEvent(
|
{
|
||||||
{
|
issueId,
|
||||||
issueId,
|
commentId,
|
||||||
commentId,
|
},
|
||||||
},
|
"ISSUE_COMMENT_DELETE",
|
||||||
"ISSUE_COMMENT_DELETE",
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -316,21 +307,20 @@ class ProjectIssuesServices extends APIService {
|
|||||||
): Promise<IIssueLabels> {
|
): Promise<IIssueLabels> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`, data)
|
||||||
.then((response: { data: IIssueLabels; [key: string]: any }) => {
|
.then((response: { data: IIssueLabels; [key: string]: any }) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueLabelEvent(
|
||||||
trackEventServices.trackIssueLabelEvent(
|
{
|
||||||
{
|
workSpaceId: response?.data?.workspace_detail?.id,
|
||||||
workSpaceId: response?.data?.workspace_detail?.id,
|
workSpaceName: response?.data?.workspace_detail?.name,
|
||||||
workSpaceName: response?.data?.workspace_detail?.name,
|
workspaceSlug,
|
||||||
workspaceSlug,
|
projectId,
|
||||||
projectId,
|
projectIdentifier: response?.data?.project_detail?.identifier,
|
||||||
projectIdentifier: response?.data?.project_detail?.identifier,
|
projectName: response?.data?.project_detail?.name,
|
||||||
projectName: response?.data?.project_detail?.name,
|
labelId: response?.data?.id,
|
||||||
labelId: response?.data?.id,
|
color: response?.data?.color,
|
||||||
color: response?.data?.color,
|
},
|
||||||
},
|
"ISSUE_LABEL_CREATE",
|
||||||
"ISSUE_LABEL_CREATE",
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -350,21 +340,20 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueLabelEvent(
|
||||||
trackEventServices.trackIssueLabelEvent(
|
{
|
||||||
{
|
workSpaceId: response?.data?.workspace_detail?.id,
|
||||||
workSpaceId: response?.data?.workspace_detail?.id,
|
workSpaceName: response?.data?.workspace_detail?.name,
|
||||||
workSpaceName: response?.data?.workspace_detail?.name,
|
workspaceSlug,
|
||||||
workspaceSlug,
|
projectId,
|
||||||
projectId,
|
projectIdentifier: response?.data?.project_detail?.identifier,
|
||||||
projectIdentifier: response?.data?.project_detail?.identifier,
|
projectName: response?.data?.project_detail?.name,
|
||||||
projectName: response?.data?.project_detail?.name,
|
labelId: response?.data?.id,
|
||||||
labelId: response?.data?.id,
|
color: response?.data?.color,
|
||||||
color: response?.data?.color,
|
},
|
||||||
},
|
"ISSUE_LABEL_UPDATE",
|
||||||
"ISSUE_LABEL_UPDATE",
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -382,15 +371,14 @@ class ProjectIssuesServices extends APIService {
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueLabelEvent(
|
||||||
trackEventServices.trackIssueLabelEvent(
|
{
|
||||||
{
|
workspaceSlug,
|
||||||
workspaceSlug,
|
projectId,
|
||||||
projectId,
|
},
|
||||||
},
|
"ISSUE_LABEL_DELETE",
|
||||||
"ISSUE_LABEL_DELETE",
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -410,7 +398,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE", user);
|
trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -426,7 +414,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`)
|
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackIssueEvent({ issuesId }, "ISSUE_DELETE", user);
|
trackEventServices.trackIssueEvent({ issuesId }, "ISSUE_DELETE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -445,7 +433,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackIssueBulkDeleteEvent(data, user);
|
trackEventServices.trackIssueBulkDeleteEvent(data, user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -629,6 +617,68 @@ class ProjectIssuesServices extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise<any> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDraftIssue(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
data: any,
|
||||||
|
user: ICurrentUserResponse
|
||||||
|
): 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,
|
||||||
|
user: ICurrentUserResponse
|
||||||
|
): 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ProjectIssuesServices();
|
const projectIssuesServices = new ProjectIssuesServices();
|
||||||
|
|
||||||
|
export default projectIssuesServices;
|
||||||
|
@ -5,11 +5,6 @@ import trackEventServices from "./track-event.service";
|
|||||||
import type { IModule, IIssue, ICurrentUserResponse } from "types";
|
import type { IModule, IIssue, ICurrentUserResponse } from "types";
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class ProjectIssuesServices extends APIService {
|
class ProjectIssuesServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -31,7 +26,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user);
|
trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -51,7 +46,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
|
trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -83,7 +78,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
|
trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -101,7 +96,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user);
|
trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -156,20 +151,19 @@ class ProjectIssuesServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
|
||||||
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
|
{
|
||||||
{
|
workspaceSlug,
|
||||||
workspaceSlug,
|
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
|
||||||
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
|
projectId,
|
||||||
projectId,
|
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
|
||||||
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
|
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
|
||||||
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
|
issueId: response?.data?.[0]?.issue_detail?.id,
|
||||||
issueId: response?.data?.[0]?.issue_detail?.id,
|
moduleId,
|
||||||
moduleId,
|
},
|
||||||
},
|
response?.data?.length > 1 ? "ISSUE_MOVED_TO_MODULE_IN_BULK" : "ISSUE_MOVED_TO_MODULE",
|
||||||
response?.data?.length > 1 ? "ISSUE_MOVED_TO_MODULE_IN_BULK" : "ISSUE_MOVED_TO_MODULE",
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -5,9 +5,6 @@ import trackEventServices from "services/track-event.service";
|
|||||||
// types
|
// types
|
||||||
import { IPage, IPageBlock, RecentPagesResponse, IIssue, ICurrentUserResponse } from "types";
|
import { IPage, IPageBlock, RecentPagesResponse, IIssue, ICurrentUserResponse } from "types";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class PageServices extends APIService {
|
class PageServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -21,7 +18,7 @@ class PageServices extends APIService {
|
|||||||
): Promise<IPage> {
|
): Promise<IPage> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_CREATE", user);
|
trackEventServices.trackPageEvent(response?.data, "PAGE_CREATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -41,7 +38,7 @@ class PageServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_UPDATE", user);
|
trackEventServices.trackPageEvent(response?.data, "PAGE_UPDATE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -57,7 +54,7 @@ class PageServices extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`)
|
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_DELETE", user);
|
trackEventServices.trackPageEvent(response?.data, "PAGE_DELETE", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -140,8 +137,7 @@ class PageServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CREATE", user);
|
||||||
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CREATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -177,8 +173,7 @@ class PageServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_UPDATE", user);
|
||||||
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_UPDATE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -197,8 +192,7 @@ class PageServices extends APIService {
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_DELETE", user);
|
||||||
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_DELETE", user);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -231,12 +225,11 @@ class PageServices extends APIService {
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${blockId}/issues/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${blockId}/issues/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackPageBlockEvent(
|
||||||
trackEventServices.trackPageBlockEvent(
|
response?.data,
|
||||||
response?.data,
|
"PAGE_BLOCK_CONVERTED_TO_ISSUE",
|
||||||
"PAGE_BLOCK_CONVERTED_TO_ISSUE",
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -6,9 +6,6 @@ import trackEventServices from "services/track-event.service";
|
|||||||
import { ICurrentUserResponse } from "types";
|
import { ICurrentUserResponse } from "types";
|
||||||
import { IProjectPublishSettings } from "store/project-publish";
|
import { IProjectPublishSettings } from "store/project-publish";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
class ProjectServices extends APIService {
|
class ProjectServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -23,13 +20,11 @@ class ProjectServices extends APIService {
|
|||||||
`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/`
|
`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) {
|
// trackEventServices.trackProjectPublishSettingsEvent(
|
||||||
// trackEventServices.trackProjectPublishSettingsEvent(
|
// response.data,
|
||||||
// response.data,
|
// "GET_PROJECT_PUBLISH_SETTINGS",
|
||||||
// "GET_PROJECT_PUBLISH_SETTINGS",
|
// user
|
||||||
// user
|
// );
|
||||||
// );
|
|
||||||
}
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -48,13 +43,12 @@ class ProjectServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) {
|
// trackEventServices.trackProjectPublishSettingsEvent(
|
||||||
// trackEventServices.trackProjectPublishSettingsEvent(
|
// response.data,
|
||||||
// response.data,
|
// "CREATE_PROJECT_PUBLISH_SETTINGS",
|
||||||
// "CREATE_PROJECT_PUBLISH_SETTINGS",
|
// user
|
||||||
// user
|
// );
|
||||||
// );
|
|
||||||
}
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -74,13 +68,11 @@ class ProjectServices extends APIService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) {
|
// trackEventServices.trackProjectPublishSettingsEvent(
|
||||||
// trackEventServices.trackProjectPublishSettingsEvent(
|
// response.data,
|
||||||
// response.data,
|
// "UPDATE_PROJECT_PUBLISH_SETTINGS",
|
||||||
// "UPDATE_PROJECT_PUBLISH_SETTINGS",
|
// user
|
||||||
// user
|
// );
|
||||||
// );
|
|
||||||
}
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -98,13 +90,11 @@ class ProjectServices extends APIService {
|
|||||||
`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/${project_publish_id}/`
|
`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/${project_publish_id}/`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) {
|
// trackEventServices.trackProjectPublishSettingsEvent(
|
||||||
// trackEventServices.trackProjectPublishSettingsEvent(
|
// response.data,
|
||||||
// response.data,
|
// "DELETE_PROJECT_PUBLISH_SETTINGS",
|
||||||
// "DELETE_PROJECT_PUBLISH_SETTINGS",
|
// user
|
||||||
// user
|
// );
|
||||||
// );
|
|
||||||
}
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -16,9 +16,6 @@ import type {
|
|||||||
TProjectIssuesSearchParams,
|
TProjectIssuesSearchParams,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
const trackEvent =
|
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
|
||||||
|
|
||||||
export class ProjectServices extends APIService {
|
export class ProjectServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
@ -31,7 +28,7 @@ export class ProjectServices extends APIService {
|
|||||||
): Promise<IProject> {
|
): Promise<IProject> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackProjectEvent(response.data, "CREATE_PROJECT", user);
|
trackEventServices.trackProjectEvent(response.data, "CREATE_PROJECT", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -82,7 +79,7 @@ export class ProjectServices extends APIService {
|
|||||||
): Promise<IProject> {
|
): Promise<IProject> {
|
||||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data)
|
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackProjectEvent(response.data, "UPDATE_PROJECT", user);
|
trackEventServices.trackProjectEvent(response.data, "UPDATE_PROJECT", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -97,7 +94,7 @@ export class ProjectServices extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackProjectEvent({ projectId }, "DELETE_PROJECT", user);
|
trackEventServices.trackProjectEvent({ projectId }, "DELETE_PROJECT", user);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -113,18 +110,17 @@ export class ProjectServices extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/add/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/add/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackProjectEvent(
|
||||||
trackEventServices.trackProjectEvent(
|
{
|
||||||
{
|
workspaceId: response?.data?.workspace?.id,
|
||||||
workspaceId: response?.data?.workspace?.id,
|
workspaceSlug,
|
||||||
workspaceSlug,
|
projectId,
|
||||||
projectId,
|
projectName: response?.data?.project?.name,
|
||||||
projectName: response?.data?.project?.name,
|
memberEmail: response?.data?.member?.email,
|
||||||
memberEmail: response?.data?.member?.email,
|
},
|
||||||
},
|
"PROJECT_MEMBER_INVITE",
|
||||||
"PROJECT_MEMBER_INVITE",
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -147,16 +143,15 @@ export class ProjectServices extends APIService {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/leave/`)
|
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/leave/`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent)
|
trackEventServices.trackProjectEvent(
|
||||||
trackEventServices.trackProjectEvent(
|
"PROJECT_MEMBER_LEAVE",
|
||||||
"PROJECT_MEMBER_LEAVE",
|
{
|
||||||
{
|
workspaceSlug,
|
||||||
workspaceSlug,
|
projectId,
|
||||||
projectId,
|
...response?.data,
|
||||||
...response?.data,
|
},
|
||||||
},
|
user
|
||||||
user
|
);
|
||||||
);
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user