Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d2bed51cc | ||
|
|
7edaa49c21 | ||
|
|
8cc61bc427 | ||
|
|
fc82d6fc23 | ||
|
|
080b5a29ae | ||
|
|
9ee3fb9c6c | ||
|
|
b0a24ab57b | ||
|
|
3e706f9653 | ||
|
|
4e86110123 | ||
|
|
6bebb8a93b | ||
|
|
c8f98a9bc2 | ||
|
|
55b2927a17 | ||
|
|
597ea26d7b | ||
|
|
4aad35e007 | ||
|
|
d95ea463b2 | ||
|
|
993b388f00 | ||
|
|
a49f00bd39 | ||
|
|
ca2da41dd2 | ||
|
|
a6d741e784 | ||
|
|
cea39c758e | ||
|
|
d72d3da6de | ||
|
|
07d548ea43 | ||
|
|
08f7ac6da7 | ||
|
|
fcf9851ee4 | ||
|
|
f6c1dad342 | ||
|
|
4c54d826ba | ||
|
|
1786a395dc | ||
|
|
d7a36f5b04 | ||
|
|
edc5a38973 | ||
|
|
38421e8106 | ||
|
|
05a76c5ee3 | ||
|
|
c739b7235d |
+3
-2
@@ -16,7 +16,8 @@ node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
dist
|
||||
dist/
|
||||
out/
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
@@ -74,7 +75,7 @@ pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
|
||||
.npmrc
|
||||
|
||||
tmp/
|
||||
|
||||
## packages
|
||||
dist
|
||||
|
||||
@@ -75,13 +75,13 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
|
||||
assignees_list = serializers.ListField(
|
||||
assignees = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
labels_list = serializers.ListField(
|
||||
labels = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
@@ -99,6 +99,12 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
|
||||
data['labels'] = [str(label.id) for label in instance.labels.all()]
|
||||
return data
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
@@ -109,8 +115,8 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
assignees = validated_data.pop("assignees", None)
|
||||
labels = validated_data.pop("labels", None)
|
||||
|
||||
project_id = self.context["project_id"]
|
||||
workspace_id = self.context["workspace_id"]
|
||||
@@ -168,8 +174,8 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
return issue
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
assignees = validated_data.pop("assignees", None)
|
||||
labels = validated_data.pop("labels", None)
|
||||
|
||||
# Related models
|
||||
project_id = instance.project_id
|
||||
|
||||
@@ -17,7 +17,7 @@ from plane.api.views import (
|
||||
IssueSubscriberViewSet,
|
||||
IssueReactionViewSet,
|
||||
CommentReactionViewSet,
|
||||
IssuePropertyViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
IssueArchiveViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueDraftViewSet,
|
||||
@@ -235,28 +235,11 @@ urlpatterns = [
|
||||
## End Comment Reactions
|
||||
## IssueProperty
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
|
||||
IssuePropertyViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-roadmap",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
|
||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||
name="project-issue-display-properties",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/<uuid:pk>/",
|
||||
IssuePropertyViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-roadmap",
|
||||
),
|
||||
## IssueProperty Ebd
|
||||
## IssueProperty End
|
||||
## Issue Archives
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
|
||||
|
||||
@@ -82,7 +82,7 @@ from plane.api.views import (
|
||||
BulkDeleteIssuesEndpoint,
|
||||
BulkImportIssuesEndpoint,
|
||||
ProjectUserViewsEndpoint,
|
||||
IssuePropertyViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
LabelViewSet,
|
||||
SubIssuesEndpoint,
|
||||
IssueLinkViewSet,
|
||||
@@ -1008,26 +1008,9 @@ urlpatterns = [
|
||||
## End Comment Reactions
|
||||
## IssueProperty
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
|
||||
IssuePropertyViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-roadmap",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/<uuid:pk>/",
|
||||
IssuePropertyViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-roadmap",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
|
||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||
name="project-issue-display-properties",
|
||||
),
|
||||
## IssueProperty Ebd
|
||||
## Issue Archives
|
||||
|
||||
@@ -71,7 +71,7 @@ from .issue import (
|
||||
WorkSpaceIssuesEndpoint,
|
||||
IssueActivityEndpoint,
|
||||
IssueCommentViewSet,
|
||||
IssuePropertyViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
LabelViewSet,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
UserWorkSpaceIssues,
|
||||
|
||||
@@ -84,6 +84,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||
capture_exception(e)
|
||||
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
print(e) if settings.DEBUG else print("Server Error")
|
||||
capture_exception(e)
|
||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@@ -161,6 +162,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
if isinstance(e, KeyError):
|
||||
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
print(e) if settings.DEBUG else print("Server Error")
|
||||
capture_exception(e)
|
||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
@@ -588,14 +588,14 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if group_by:
|
||||
grouped_results = group_results(issues_data, group_by, sub_group_by)
|
||||
return Response(
|
||||
group_results(issues_data, group_by, sub_group_by),
|
||||
grouped_results,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(
|
||||
issues_data,
|
||||
status=status.HTTP_200_OK,
|
||||
issues_data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, cycle_id):
|
||||
|
||||
@@ -130,7 +130,7 @@ class IssueViewSet(BaseViewSet):
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id):
|
||||
@@ -229,12 +229,16 @@ class IssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if group_by:
|
||||
grouped_results = group_results(issues, group_by, sub_group_by)
|
||||
return Response(
|
||||
group_results(issues, group_by, sub_group_by),
|
||||
grouped_results,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
issues, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
@@ -433,12 +437,15 @@ class UserWorkSpaceIssues(BaseAPIView):
|
||||
)
|
||||
|
||||
if group_by:
|
||||
grouped_results = group_results(issues, group_by, sub_group_by)
|
||||
return Response(
|
||||
group_results(issues, group_by, sub_group_by),
|
||||
grouped_results,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
issues, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
class WorkSpaceIssuesEndpoint(BaseAPIView):
|
||||
@@ -597,41 +604,12 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class IssuePropertyViewSet(BaseViewSet):
|
||||
serializer_class = IssuePropertySerializer
|
||||
model = IssueProperty
|
||||
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
filterset_fields = []
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"), user=self.request.user
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(user=self.request.user)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
serializer = IssuePropertySerializer(queryset, many=True)
|
||||
return Response(
|
||||
serializer.data[0] if len(serializer.data) > 0 else [],
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
def post(self, request, slug, project_id):
|
||||
issue_property, created = IssueProperty.objects.get_or_create(
|
||||
user=request.user,
|
||||
project_id=project_id,
|
||||
@@ -640,16 +618,20 @@ class IssuePropertyViewSet(BaseViewSet):
|
||||
if not created:
|
||||
issue_property.properties = request.data.get("properties", {})
|
||||
issue_property.save()
|
||||
|
||||
serializer = IssuePropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
issue_property.properties = request.data.get("properties", {})
|
||||
issue_property.save()
|
||||
serializer = IssuePropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
issue_property, _ = IssueProperty.objects.get_or_create(
|
||||
user=request.user, project_id=project_id
|
||||
)
|
||||
serializer = IssuePropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class LabelViewSet(BaseViewSet):
|
||||
serializer_class = LabelSerializer
|
||||
model = Label
|
||||
@@ -963,8 +945,8 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
issue_attachments = IssueAttachment.objects.filter(
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serilaizer = IssueAttachmentSerializer(issue_attachments, many=True)
|
||||
return Response(serilaizer.data, status=status.HTTP_200_OK)
|
||||
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueArchiveViewSet(BaseViewSet):
|
||||
@@ -1165,9 +1147,7 @@ class IssueSubscriberViewSet(BaseViewSet):
|
||||
|
||||
def list(self, request, slug, project_id, issue_id):
|
||||
members = (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
ProjectMember.objects.filter(workspace__slug=slug, project_id=project_id)
|
||||
.annotate(
|
||||
is_subscribed=Exists(
|
||||
IssueSubscriber.objects.filter(
|
||||
@@ -2174,9 +2154,15 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
if group_by:
|
||||
return Response(group_results(issues, group_by), status=status.HTTP_200_OK)
|
||||
grouped_results = group_results(issues, group_by)
|
||||
return Response(
|
||||
grouped_results,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
issues, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
@@ -149,6 +149,9 @@ class ModuleViewSet(BaseViewSet):
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
module = Module.objects.get(pk=serializer.data["id"])
|
||||
serializer = ModuleSerializer(module)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -361,7 +364,6 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
issues_data = IssueStateSerializer(issues, many=True).data
|
||||
|
||||
if sub_group_by and sub_group_by == group_by:
|
||||
@@ -371,14 +373,14 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if group_by:
|
||||
grouped_results = group_results(issues_data, group_by, sub_group_by)
|
||||
return Response(
|
||||
group_results(issues_data, group_by, sub_group_by),
|
||||
grouped_results,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(
|
||||
issues_data,
|
||||
status=status.HTTP_200_OK,
|
||||
issues_data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, module_id):
|
||||
|
||||
@@ -69,6 +69,7 @@ from plane.db.models import (
|
||||
ModuleMember,
|
||||
Inbox,
|
||||
ProjectDeployBoard,
|
||||
IssueProperty,
|
||||
)
|
||||
|
||||
from plane.bgtasks.project_invitation_task import project_invitation
|
||||
@@ -201,6 +202,11 @@ class ProjectViewSet(BaseViewSet):
|
||||
project_member = ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"], member=request.user, role=20
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueProperty.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
if serializer.data["project_lead"] is not None and str(
|
||||
serializer.data["project_lead"]
|
||||
@@ -210,6 +216,11 @@ class ProjectViewSet(BaseViewSet):
|
||||
member_id=serializer.data["project_lead"],
|
||||
role=20,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
IssueProperty.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
user_id=serializer.data["project_lead"],
|
||||
)
|
||||
|
||||
# Default states
|
||||
states = [
|
||||
@@ -262,12 +273,9 @@ class ProjectViewSet(BaseViewSet):
|
||||
]
|
||||
)
|
||||
|
||||
data = serializer.data
|
||||
# Additional fields of the member
|
||||
data["sort_order"] = project_member.sort_order
|
||||
data["member_role"] = project_member.role
|
||||
data["is_member"] = True
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||
serializer = ProjectListSerializer(project)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
serializer.errors,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -317,6 +325,8 @@ class ProjectViewSet(BaseViewSet):
|
||||
color="#ff7700",
|
||||
)
|
||||
|
||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||
serializer = ProjectListSerializer(project)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -393,6 +403,8 @@ class InviteProjectEndpoint(BaseAPIView):
|
||||
member=user, project_id=project_id, role=role
|
||||
)
|
||||
|
||||
_ = IssueProperty.objects.create(user=user, project_id=project_id)
|
||||
|
||||
return Response(
|
||||
ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK
|
||||
)
|
||||
@@ -428,6 +440,18 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
]
|
||||
)
|
||||
|
||||
IssueProperty.objects.bulk_create(
|
||||
[
|
||||
ProjectMember(
|
||||
project=invitation.project,
|
||||
workspace=invitation.project.workspace,
|
||||
user=request.user,
|
||||
created_by=request.user,
|
||||
)
|
||||
for invitation in project_invitations
|
||||
]
|
||||
)
|
||||
|
||||
# Delete joined project invites
|
||||
project_invitations.delete()
|
||||
|
||||
@@ -560,6 +584,7 @@ class AddMemberToProjectEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
bulk_project_members = []
|
||||
bulk_issue_props = []
|
||||
|
||||
project_members = (
|
||||
ProjectMember.objects.filter(
|
||||
@@ -574,7 +599,8 @@ class AddMemberToProjectEndpoint(BaseAPIView):
|
||||
sort_order = [
|
||||
project_member.get("sort_order")
|
||||
for project_member in project_members
|
||||
if str(project_member.get("member_id")) == str(member.get("member_id"))
|
||||
if str(project_member.get("member_id"))
|
||||
== str(member.get("member_id"))
|
||||
]
|
||||
bulk_project_members.append(
|
||||
ProjectMember(
|
||||
@@ -585,6 +611,13 @@ class AddMemberToProjectEndpoint(BaseAPIView):
|
||||
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
|
||||
)
|
||||
)
|
||||
bulk_issue_props.append(
|
||||
IssueProperty(
|
||||
user_id=member.get("member_id"),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
)
|
||||
)
|
||||
|
||||
project_members = ProjectMember.objects.bulk_create(
|
||||
bulk_project_members,
|
||||
@@ -592,7 +625,12 @@ class AddMemberToProjectEndpoint(BaseAPIView):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
_ = IssueProperty.objects.bulk_create(
|
||||
bulk_issue_props, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@@ -614,6 +652,7 @@ class AddTeamToProjectEndpoint(BaseAPIView):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
project_members = []
|
||||
issue_props = []
|
||||
for member in team_members:
|
||||
project_members.append(
|
||||
ProjectMember(
|
||||
@@ -623,11 +662,23 @@ class AddTeamToProjectEndpoint(BaseAPIView):
|
||||
created_by=request.user,
|
||||
)
|
||||
)
|
||||
issue_props.append(
|
||||
IssueProperty(
|
||||
project_id=project_id,
|
||||
user_id=member,
|
||||
workspace=workspace,
|
||||
created_by=request.user,
|
||||
)
|
||||
)
|
||||
|
||||
ProjectMember.objects.bulk_create(
|
||||
project_members, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
_ = IssueProperty.objects.bulk_create(
|
||||
issue_props, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -743,6 +794,19 @@ class ProjectJoinEndpoint(BaseAPIView):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
IssueProperty.objects.bulk_create(
|
||||
[
|
||||
IssueProperty(
|
||||
project_id=project_id,
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
created_by=request.user,
|
||||
)
|
||||
for project_id in project_ids
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Projects joined successfully"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
|
||||
@@ -93,7 +93,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
@@ -117,9 +116,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -129,9 +126,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
@@ -183,7 +178,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||
|
||||
## Grouping the results
|
||||
@@ -194,10 +188,12 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
{"error": "Group by and sub group by cannot be same"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
if group_by:
|
||||
grouped_results = group_results(issues, group_by, sub_group_by)
|
||||
return Response(
|
||||
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||
grouped_results,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1228,9 +1228,15 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
if group_by:
|
||||
return Response(group_results(issues, group_by), status=status.HTTP_200_OK)
|
||||
grouped_results = group_results(issues, group_by)
|
||||
return Response(
|
||||
grouped_results,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
issues, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||
|
||||
@@ -25,6 +25,7 @@ from plane.db.models import (
|
||||
WorkspaceIntegration,
|
||||
Label,
|
||||
User,
|
||||
IssueProperty,
|
||||
)
|
||||
from .workspace_invitation_task import workspace_invitation
|
||||
from plane.bgtasks.user_welcome_task import send_welcome_slack
|
||||
@@ -103,6 +104,20 @@ def service_importer(service, importer_id):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
IssueProperty.objects.bulk_create(
|
||||
[
|
||||
IssueProperty(
|
||||
project_id=importer.project_id,
|
||||
workspace_id=importer.workspace_id,
|
||||
user=user,
|
||||
created_by=importer.created_by,
|
||||
)
|
||||
for user in workspace_users
|
||||
],
|
||||
batch_size=100,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Check if sync config is on for github importers
|
||||
if service == "github" and importer.config.get("sync", False):
|
||||
name = importer.metadata.get("name", False)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,102 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import IssueSubscriber, Project, IssueAssignee, Issue, Notification
|
||||
|
||||
# Third Party imports
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task
|
||||
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created):
|
||||
issue_activities_created = (
|
||||
json.loads(issue_activities_created) if issue_activities_created is not None else None
|
||||
)
|
||||
if type not in [
|
||||
"cycle.activity.created",
|
||||
"cycle.activity.deleted",
|
||||
"module.activity.created",
|
||||
"module.activity.deleted",
|
||||
"issue_reaction.activity.created",
|
||||
"issue_reaction.activity.deleted",
|
||||
"comment_reaction.activity.created",
|
||||
"comment_reaction.activity.deleted",
|
||||
"issue_vote.activity.created",
|
||||
"issue_vote.activity.deleted",
|
||||
"issue_draft.activity.created",
|
||||
"issue_draft.activity.updated",
|
||||
"issue_draft.activity.deleted",
|
||||
]:
|
||||
# Create Notifications
|
||||
bulk_notifications = []
|
||||
issue_subscribers = list(
|
||||
IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id)
|
||||
.exclude(subscriber_id=actor_id)
|
||||
.values_list("subscriber", flat=True)
|
||||
)
|
||||
|
||||
issue_assignees = list(
|
||||
IssueAssignee.objects.filter(project_id=project_id, issue_id=issue_id)
|
||||
.exclude(assignee_id=actor_id)
|
||||
.values_list("assignee", flat=True)
|
||||
)
|
||||
|
||||
issue_subscribers = issue_subscribers + issue_assignees
|
||||
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
|
||||
if subscriber:
|
||||
# add the user to issue subscriber
|
||||
try:
|
||||
_ = IssueSubscriber.objects.get_or_create(
|
||||
issue_id=issue_id, subscriber_id=actor_id
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
for subscriber in list(set(issue_subscribers)):
|
||||
for issue_activity in issue_activities_created:
|
||||
bulk_notifications.append(
|
||||
Notification(
|
||||
workspace=project.workspace,
|
||||
sender="in_app:issue_activities",
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=subscriber,
|
||||
entity_identifier=issue_id,
|
||||
entity_name="issue",
|
||||
project=project,
|
||||
title=issue_activity.get("comment"),
|
||||
data={
|
||||
"issue": {
|
||||
"id": str(issue_id),
|
||||
"name": str(issue.name),
|
||||
"identifier": str(issue.project.identifier),
|
||||
"sequence_id": issue.sequence_id,
|
||||
"state_name": issue.state.name,
|
||||
"state_group": issue.state.group,
|
||||
},
|
||||
"issue_activity": {
|
||||
"id": str(issue_activity.get("id")),
|
||||
"verb": str(issue_activity.get("verb")),
|
||||
"field": str(issue_activity.get("field")),
|
||||
"actor": str(issue_activity.get("actor_id")),
|
||||
"new_value": str(issue_activity.get("new_value")),
|
||||
"old_value": str(issue_activity.get("old_value")),
|
||||
"issue_comment": str(
|
||||
issue_activity.get("issue_comment").comment_stripped
|
||||
if issue_activity.get("issue_comment") is not None
|
||||
else ""
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Bulk create notifications
|
||||
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-18 12:04
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import plane.db.models.issue
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='issueproperty',
|
||||
name='properties',
|
||||
field=models.JSONField(default=plane.db.models.issue.get_default_properties),
|
||||
),
|
||||
]
|
||||
@@ -16,6 +16,24 @@ from . import ProjectBaseModel
|
||||
from plane.utils.html_processor import strip_tags
|
||||
|
||||
|
||||
def get_default_properties():
|
||||
return {
|
||||
"assignee": True,
|
||||
"start_date": True,
|
||||
"due_date": True,
|
||||
"labels": True,
|
||||
"key": True,
|
||||
"priority": True,
|
||||
"state": True,
|
||||
"sub_issue_count": True,
|
||||
"link": True,
|
||||
"attachment_count": True,
|
||||
"estimate": True,
|
||||
"created_on": True,
|
||||
"updated_on": True,
|
||||
}
|
||||
|
||||
|
||||
# TODO: Handle identifiers for Bulk Inserts - nk
|
||||
class IssueManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
@@ -39,7 +57,7 @@ class Issue(ProjectBaseModel):
|
||||
("high", "High"),
|
||||
("medium", "Medium"),
|
||||
("low", "Low"),
|
||||
("none", "None")
|
||||
("none", "None"),
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
@@ -186,7 +204,7 @@ class IssueRelation(ProjectBaseModel):
|
||||
("relates_to", "Relates To"),
|
||||
("blocked_by", "Blocked By"),
|
||||
)
|
||||
|
||||
|
||||
issue = models.ForeignKey(
|
||||
Issue, related_name="issue_relation", on_delete=models.CASCADE
|
||||
)
|
||||
@@ -208,7 +226,7 @@ class IssueRelation(ProjectBaseModel):
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.related_issue.name}"
|
||||
return f"{self.issue.name} {self.related_issue.name}"
|
||||
|
||||
|
||||
class IssueAssignee(ProjectBaseModel):
|
||||
@@ -327,7 +345,9 @@ class IssueComment(ProjectBaseModel):
|
||||
comment_json = models.JSONField(blank=True, default=dict)
|
||||
comment_html = models.TextField(blank=True, default="<p></p>")
|
||||
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
|
||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments")
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.CASCADE, related_name="issue_comments"
|
||||
)
|
||||
# System can also create comment
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
@@ -367,7 +387,7 @@ class IssueProperty(ProjectBaseModel):
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_property_user",
|
||||
)
|
||||
properties = models.JSONField(default=dict)
|
||||
properties = models.JSONField(default=get_default_properties)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Property"
|
||||
@@ -515,7 +535,10 @@ class IssueVote(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "actor",]
|
||||
unique_together = [
|
||||
"issue",
|
||||
"actor",
|
||||
]
|
||||
verbose_name = "Issue Vote"
|
||||
verbose_name_plural = "Issue Votes"
|
||||
db_table = "issue_votes"
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from .base import BaseModel
|
||||
from plane.db.models import ProjectBaseModel
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
@@ -22,15 +20,8 @@ class Notification(BaseModel):
|
||||
message_html = models.TextField(blank=True, default="<p></p>")
|
||||
message_stripped = models.TextField(blank=True, null=True)
|
||||
sender = models.CharField(max_length=255)
|
||||
triggered_by = models.ForeignKey(
|
||||
"db.User",
|
||||
related_name="triggered_notifications",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
)
|
||||
receiver = models.ForeignKey(
|
||||
"db.User", related_name="received_notifications", on_delete=models.CASCADE
|
||||
)
|
||||
triggered_by = models.ForeignKey("db.User", related_name="triggered_notifications", on_delete=models.SET_NULL, null=True)
|
||||
receiver = models.ForeignKey("db.User", related_name="received_notifications", on_delete=models.CASCADE)
|
||||
read_at = models.DateTimeField(null=True)
|
||||
snoozed_till = models.DateTimeField(null=True)
|
||||
archived_at = models.DateTimeField(null=True)
|
||||
@@ -44,43 +35,3 @@ class Notification(BaseModel):
|
||||
def __str__(self):
|
||||
"""Return name of the notifications"""
|
||||
return f"{self.receiver.email} <{self.workspace.name}>"
|
||||
|
||||
|
||||
def get_default_preference():
|
||||
return {
|
||||
"property_change": {
|
||||
"email": True,
|
||||
},
|
||||
"state": {
|
||||
"email": True,
|
||||
},
|
||||
"comment": {
|
||||
"email": True,
|
||||
},
|
||||
"mentions": {
|
||||
"email": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class NotificationPreference(ProjectBaseModel):
|
||||
created_by = models.JSONField(default=get_default_preference)
|
||||
assigned = models.JSONField(default=get_default_preference)
|
||||
subscribed = models.JSONField(default=get_default_preference)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="notification_preferences",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "user"]
|
||||
|
||||
verbose_name = "Notification Preference"
|
||||
verbose_name_plural = "Notification Preferences"
|
||||
db_table = "notification_preferences"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the notifications"""
|
||||
return f"{self.user.email} <{self.workspace.name}>"
|
||||
|
||||
@@ -14,19 +14,21 @@ from .common import * # noqa
|
||||
# Database
|
||||
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": "plane",
|
||||
"USER": os.environ.get("PGUSER", ""),
|
||||
"PASSWORD": os.environ.get("PGPASSWORD", ""),
|
||||
"HOST": os.environ.get("PGHOST", ""),
|
||||
if bool(os.environ.get("DATABASE_URL")):
|
||||
# Parse database configuration from $DATABASE_URL
|
||||
DATABASES["default"] = dj_database_url.config()
|
||||
else:
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.environ.get("POSTGRES_DB"),
|
||||
"USER": os.environ.get("POSTGRES_USER"),
|
||||
"PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
|
||||
"HOST": os.environ.get("POSTGRES_HOST"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Parse database configuration from $DATABASE_URL
|
||||
DATABASES["default"] = dj_database_url.config()
|
||||
SITE_ID = 1
|
||||
|
||||
# Set the variable true if running in docker environment
|
||||
@@ -278,4 +280,3 @@ SCOUT_NAME = "Plane"
|
||||
|
||||
# Unsplash Access key
|
||||
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import re
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
# The date from pattern
|
||||
pattern = re.compile(r"\d+_(weeks|months)$")
|
||||
|
||||
# check the valid uuids
|
||||
def filter_valid_uuids(uuid_list):
|
||||
valid_uuids = []
|
||||
for uuid_str in uuid_list:
|
||||
try:
|
||||
uuid_obj = uuid.UUID(uuid_str)
|
||||
valid_uuids.append(uuid_obj)
|
||||
except ValueError:
|
||||
# ignore the invalid uuids
|
||||
pass
|
||||
return valid_uuids
|
||||
|
||||
|
||||
# Get the 2_weeks, 3_months
|
||||
def string_date_filter(filter, duration, subsequent, term, date_filter, offset):
|
||||
@@ -61,40 +75,41 @@ def date_filter(filter, date_term, queries):
|
||||
|
||||
def filter_state(params, filter, method):
|
||||
if method == "GET":
|
||||
states = params.get("state").split(",")
|
||||
states = [item for item in params.get("state").split(",") if item != 'null']
|
||||
states = filter_valid_uuids(states)
|
||||
if len(states) and "" not in states:
|
||||
filter["state__in"] = states
|
||||
else:
|
||||
if params.get("state", None) and len(params.get("state")):
|
||||
if params.get("state", None) and len(params.get("state")) and params.get("state") != 'null':
|
||||
filter["state__in"] = params.get("state")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_state_group(params, filter, method):
|
||||
if method == "GET":
|
||||
state_group = params.get("state_group").split(",")
|
||||
state_group = [item for item in params.get("state_group").split(",") if item != 'null']
|
||||
if len(state_group) and "" not in state_group:
|
||||
filter["state__group__in"] = state_group
|
||||
else:
|
||||
if params.get("state_group", None) and len(params.get("state_group")):
|
||||
if params.get("state_group", None) and len(params.get("state_group")) and params.get("state_group") != 'null':
|
||||
filter["state__group__in"] = params.get("state_group")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_estimate_point(params, filter, method):
|
||||
if method == "GET":
|
||||
estimate_points = params.get("estimate_point").split(",")
|
||||
estimate_points = [item for item in params.get("estimate_point").split(",") if item != 'null']
|
||||
if len(estimate_points) and "" not in estimate_points:
|
||||
filter["estimate_point__in"] = estimate_points
|
||||
else:
|
||||
if params.get("estimate_point", None) and len(params.get("estimate_point")):
|
||||
if params.get("estimate_point", None) and len(params.get("estimate_point")) and params.get("estimate_point") != 'null':
|
||||
filter["estimate_point__in"] = params.get("estimate_point")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_priority(params, filter, method):
|
||||
if method == "GET":
|
||||
priorities = params.get("priority").split(",")
|
||||
priorities = [item for item in params.get("priority").split(",") if item != 'null']
|
||||
if len(priorities) and "" not in priorities:
|
||||
filter["priority__in"] = priorities
|
||||
return filter
|
||||
@@ -102,44 +117,48 @@ def filter_priority(params, filter, method):
|
||||
|
||||
def filter_parent(params, filter, method):
|
||||
if method == "GET":
|
||||
parents = params.get("parent").split(",")
|
||||
parents = [item for item in params.get("parent").split(",") if item != 'null']
|
||||
parents = filter_valid_uuids(parents)
|
||||
if len(parents) and "" not in parents:
|
||||
filter["parent__in"] = parents
|
||||
else:
|
||||
if params.get("parent", None) and len(params.get("parent")):
|
||||
if params.get("parent", None) and len(params.get("parent")) and params.get("parent") != 'null':
|
||||
filter["parent__in"] = params.get("parent")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_labels(params, filter, method):
|
||||
if method == "GET":
|
||||
labels = params.get("labels").split(",")
|
||||
labels = [item for item in params.get("labels").split(",") if item != 'null']
|
||||
labels = filter_valid_uuids(labels)
|
||||
if len(labels) and "" not in labels:
|
||||
filter["labels__in"] = labels
|
||||
else:
|
||||
if params.get("labels", None) and len(params.get("labels")):
|
||||
if params.get("labels", None) and len(params.get("labels")) and params.get("labels") != 'null':
|
||||
filter["labels__in"] = params.get("labels")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_assignees(params, filter, method):
|
||||
if method == "GET":
|
||||
assignees = params.get("assignees").split(",")
|
||||
assignees = [item for item in params.get("assignees").split(",") if item != 'null']
|
||||
assignees = filter_valid_uuids(assignees)
|
||||
if len(assignees) and "" not in assignees:
|
||||
filter["assignees__in"] = assignees
|
||||
else:
|
||||
if params.get("assignees", None) and len(params.get("assignees")):
|
||||
if params.get("assignees", None) and len(params.get("assignees")) and params.get("assignees") != 'null':
|
||||
filter["assignees__in"] = params.get("assignees")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_created_by(params, filter, method):
|
||||
if method == "GET":
|
||||
created_bys = params.get("created_by").split(",")
|
||||
created_bys = [item for item in params.get("created_by").split(",") if item != 'null']
|
||||
created_bys = filter_valid_uuids(created_bys)
|
||||
if len(created_bys) and "" not in created_bys:
|
||||
filter["created_by__in"] = created_bys
|
||||
else:
|
||||
if params.get("created_by", None) and len(params.get("created_by")):
|
||||
if params.get("created_by", None) and len(params.get("created_by")) and params.get("created_by") != 'null':
|
||||
filter["created_by__in"] = params.get("created_by")
|
||||
return filter
|
||||
|
||||
@@ -219,44 +238,47 @@ def filter_issue_state_type(params, filter, method):
|
||||
|
||||
def filter_project(params, filter, method):
|
||||
if method == "GET":
|
||||
projects = params.get("project").split(",")
|
||||
projects = [item for item in params.get("project").split(",") if item != 'null']
|
||||
projects = filter_valid_uuids(projects)
|
||||
if len(projects) and "" not in projects:
|
||||
filter["project__in"] = projects
|
||||
else:
|
||||
if params.get("project", None) and len(params.get("project")):
|
||||
if params.get("project", None) and len(params.get("project")) and params.get("project") != 'null':
|
||||
filter["project__in"] = params.get("project")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_cycle(params, filter, method):
|
||||
if method == "GET":
|
||||
cycles = params.get("cycle").split(",")
|
||||
cycles = [item for item in params.get("cycle").split(",") if item != 'null']
|
||||
cycles = filter_valid_uuids(cycles)
|
||||
if len(cycles) and "" not in cycles:
|
||||
filter["issue_cycle__cycle_id__in"] = cycles
|
||||
else:
|
||||
if params.get("cycle", None) and len(params.get("cycle")):
|
||||
if params.get("cycle", None) and len(params.get("cycle")) and params.get("cycle") != 'null':
|
||||
filter["issue_cycle__cycle_id__in"] = params.get("cycle")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_module(params, filter, method):
|
||||
if method == "GET":
|
||||
modules = params.get("module").split(",")
|
||||
modules = [item for item in params.get("module").split(",") if item != 'null']
|
||||
modules = filter_valid_uuids(modules)
|
||||
if len(modules) and "" not in modules:
|
||||
filter["issue_module__module_id__in"] = modules
|
||||
else:
|
||||
if params.get("module", None) and len(params.get("module")):
|
||||
if params.get("module", None) and len(params.get("module")) and params.get("module") != 'null':
|
||||
filter["issue_module__module_id__in"] = params.get("module")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_inbox_status(params, filter, method):
|
||||
if method == "GET":
|
||||
status = params.get("inbox_status").split(",")
|
||||
status = [item for item in params.get("inbox_status").split(",") if item != 'null']
|
||||
if len(status) and "" not in status:
|
||||
filter["issue_inbox__status__in"] = status
|
||||
else:
|
||||
if params.get("inbox_status", None) and len(params.get("inbox_status")):
|
||||
if params.get("inbox_status", None) and len(params.get("inbox_status")) and params.get("inbox_status") != 'null':
|
||||
filter["issue_inbox__status__in"] = params.get("inbox_status")
|
||||
return filter
|
||||
|
||||
@@ -275,11 +297,12 @@ def filter_sub_issue_toggle(params, filter, method):
|
||||
|
||||
def filter_subscribed_issues(params, filter, method):
|
||||
if method == "GET":
|
||||
subscribers = params.get("subscriber").split(",")
|
||||
subscribers = [item for item in params.get("subscriber").split(",") if item != 'null']
|
||||
subscribers = filter_valid_uuids(subscribers)
|
||||
if len(subscribers) and "" not in subscribers:
|
||||
filter["issue_subscribers__subscriber_id__in"] = subscribers
|
||||
else:
|
||||
if params.get("subscriber", None) and len(params.get("subscriber")):
|
||||
if params.get("subscriber", None) and len(params.get("subscriber")) and params.get("subscriber") != 'null':
|
||||
filter["issue_subscribers__subscriber_id__in"] = params.get("subscriber")
|
||||
return filter
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Deploy the Plane image
|
||||
FROM makeplane/plane
|
||||
|
||||
LABEL maintainer="engineering@plane.so"
|
||||
@@ -0,0 +1,168 @@
|
||||
version: "3.8"
|
||||
|
||||
x-app-env : &app-env
|
||||
environment:
|
||||
- NGINX_PORT=${NGINX_PORT:-84}
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
|
||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
|
||||
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||
- DOCKERIZED=${DOCKERIZED:-1}
|
||||
#DB SETTINGS
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-plane}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-plane}
|
||||
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PGHOST}/${PGDATABASE}}
|
||||
# REDIS SETTINGS
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
|
||||
# EMAIL SETTINGS
|
||||
- EMAIL_HOST=${EMAIL_HOST:-""}
|
||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
||||
- EMAIL_FROM=${EMAIL_FROM:-"Team Plane <team@mailer.plane.so>"}
|
||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
||||
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
||||
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||
# OPENAI SETTINGS
|
||||
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"}
|
||||
- GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"}
|
||||
# LOGIN/SIGNUP SETTINGS
|
||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
|
||||
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
# DATA STORE SETTINGS
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- AWS_REGION=${AWS_REGION:-""}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-"access-key"}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-"secret-key"}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-"access-key"}
|
||||
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
|
||||
- BUCKET_NAME=${BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
|
||||
services:
|
||||
web:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-frontend:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
deploy:
|
||||
replicas: ${WEB_REPLICAS:-1}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
|
||||
space:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-space:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
deploy:
|
||||
replicas: ${SPACE_REPLICAS:-1}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
api:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: ./bin/takeoff
|
||||
deploy:
|
||||
replicas: ${API_REPLICAS:-1}
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
worker:
|
||||
<<: *app-env
|
||||
container_name: bgworker
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: ./bin/worker
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
beat-worker:
|
||||
<<: *app-env
|
||||
container_name: beatworker
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: ./bin/beat
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-db:
|
||||
<<: *app-env
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
restart: unless-stopped
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
plane-redis:
|
||||
<<: *app-env
|
||||
container_name: plane-redis
|
||||
image: redis:6.2.7-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
plane-minio:
|
||||
<<: *app-env
|
||||
container_name: plane-minio
|
||||
image: minio/minio
|
||||
restart: unless-stopped
|
||||
command: server /export --console-address ":9090"
|
||||
volumes:
|
||||
- uploads:/export
|
||||
|
||||
createbuckets:
|
||||
<<: *app-env
|
||||
image: minio/mc
|
||||
entrypoint: >
|
||||
/bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
|
||||
depends_on:
|
||||
- plane-minio
|
||||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
proxy:
|
||||
<<: *app-env
|
||||
container_name: proxy
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-proxy:${APP_RELEASE:-latest}
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
depends_on:
|
||||
- web
|
||||
- api
|
||||
- space
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
uploads:
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
|
||||
BRANCH=${BRANCH:-master}
|
||||
SCRIPT_DIR=$PWD
|
||||
PLANE_INSTALL_DIR=$PWD/plane-app
|
||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
|
||||
function install(){
|
||||
echo
|
||||
echo "Installing on $PLANE_INSTALL_DIR"
|
||||
download
|
||||
}
|
||||
function download(){
|
||||
cd $SCRIPT_DIR
|
||||
TS=$(date +%s)
|
||||
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
|
||||
then
|
||||
mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml
|
||||
fi
|
||||
|
||||
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/docker-compose.yaml https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s)
|
||||
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/variables-upgrade.env https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s)
|
||||
|
||||
if [ -f "$PLANE_INSTALL_DIR/.env" ];
|
||||
then
|
||||
cp $PLANE_INSTALL_DIR/.env $PLANE_INSTALL_DIR/archive/$TS.env
|
||||
else
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env
|
||||
fi
|
||||
|
||||
|
||||
echo ""
|
||||
echo "Latest version is now available for you to use"
|
||||
echo ""
|
||||
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||
echo ""
|
||||
|
||||
}
|
||||
function startServices(){
|
||||
cd $PLANE_INSTALL_DIR
|
||||
docker compose up -d
|
||||
cd $SCRIPT_DIR
|
||||
}
|
||||
function stopServices(){
|
||||
cd $PLANE_INSTALL_DIR
|
||||
docker compose down
|
||||
cd $SCRIPT_DIR
|
||||
}
|
||||
function restartServices(){
|
||||
cd $PLANE_INSTALL_DIR
|
||||
docker compose restart
|
||||
cd $SCRIPT_DIR
|
||||
}
|
||||
function upgrade(){
|
||||
echo "***** STOPPING SERVICES ****"
|
||||
stopServices
|
||||
|
||||
echo
|
||||
echo "***** DOWNLOADING LATEST VERSION ****"
|
||||
download
|
||||
|
||||
echo "***** PLEASE VALIDATE AND START SERVICES ****"
|
||||
|
||||
}
|
||||
function askForAction(){
|
||||
echo
|
||||
echo "Select a Action you want to perform:"
|
||||
echo " 1) Install"
|
||||
echo " 2) Start"
|
||||
echo " 3) Stop"
|
||||
echo " 4) Restart"
|
||||
echo " 5) Upgrade"
|
||||
echo " 6) Exit"
|
||||
echo
|
||||
read -p "Action [2]: " ACTION
|
||||
until [[ -z "$ACTION" || "$ACTION" =~ ^[1-6]$ ]]; do
|
||||
echo "$ACTION: invalid selection."
|
||||
read -p "Action [2]: " ACTION
|
||||
done
|
||||
echo
|
||||
|
||||
|
||||
if [ "$ACTION" == "1" ]
|
||||
then
|
||||
install
|
||||
askForAction
|
||||
elif [ "$ACTION" == "2" ] || [ "$ACTION" == "" ]
|
||||
then
|
||||
startServices
|
||||
askForAction
|
||||
elif [ "$ACTION" == "3" ]
|
||||
then
|
||||
stopServices
|
||||
askForAction
|
||||
elif [ "$ACTION" == "4" ]
|
||||
then
|
||||
restartServices
|
||||
askForAction
|
||||
elif [ "$ACTION" == "5" ]
|
||||
then
|
||||
upgrade
|
||||
askForAction
|
||||
elif [ "$ACTION" == "6" ]
|
||||
then
|
||||
exit 0
|
||||
else
|
||||
echo "INVALID ACTION SUPPLIED"
|
||||
fi
|
||||
}
|
||||
|
||||
askForAction
|
||||
@@ -0,0 +1,63 @@
|
||||
APP_RELEASE=latest
|
||||
|
||||
WEB_REPLICAS=1
|
||||
SPACE_REPLICAS=1
|
||||
API_REPLICAS=1
|
||||
|
||||
NGINX_PORT=80
|
||||
DEBUG=0
|
||||
DJANGO_SETTINGS_MODULE=plane.settings.selfhosted
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
||||
SENTRY_DSN=""
|
||||
GITHUB_CLIENT_SECRET=""
|
||||
DOCKERIZED=1
|
||||
|
||||
#DB SETTINGS
|
||||
PGHOST=plane-db
|
||||
PGDATABASE=plane
|
||||
POSTGRES_USER=plane
|
||||
POSTGRES_PASSWORD=plane
|
||||
POSTGRES_DB=plane
|
||||
PGDATA=/var/lib/postgresql/data
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
|
||||
# REDIS SETTINGS
|
||||
REDIS_HOST=plane-redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_URL=redis://${REDIS_HOST}:6379/
|
||||
|
||||
# EMAIL SETTINGS
|
||||
EMAIL_HOST=""
|
||||
EMAIL_HOST_USER=""
|
||||
EMAIL_HOST_PASSWORD=""
|
||||
EMAIL_PORT=587
|
||||
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||
EMAIL_USE_TLS=1
|
||||
EMAIL_USE_SSL=0
|
||||
DEFAULT_EMAIL=captain@plane.so
|
||||
DEFAULT_PASSWORD=password123
|
||||
|
||||
# OPENAI SETTINGS
|
||||
OPENAI_API_BASE=https://api.openai.com/v1
|
||||
OPENAI_API_KEY="sk-"
|
||||
GPT_ENGINE="gpt-3.5-turbo"
|
||||
|
||||
# LOGIN/SIGNUP SETTINGS
|
||||
ENABLE_SIGNUP=1
|
||||
ENABLE_EMAIL_PASSWORD=1
|
||||
ENABLE_MAGIC_LINK_LOGIN=0
|
||||
SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
|
||||
|
||||
# DATA STORE SETTINGS
|
||||
USE_MINIO=1
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID="access-key"
|
||||
AWS_SECRET_ACCESS_KEY="secret-key"
|
||||
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
|
||||
AWS_S3_BUCKET_NAME=uploads
|
||||
MINIO_ROOT_USER="access-key"
|
||||
MINIO_ROOT_PASSWORD="secret-key"
|
||||
BUCKET_NAME=uploads
|
||||
FILE_SIZE_LIMIT=5242880
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
container_name: web
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-frontend:latest
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
environment:
|
||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
|
||||
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
|
||||
space:
|
||||
container_name: space
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-space:latest
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
environment:
|
||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
api:
|
||||
container_name: api
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/takeoff
|
||||
environment:
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- PGUSER=${PGUSER:-plane}
|
||||
- PGPASSWORD=${PGPASSWORD:-plane}
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}}
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
|
||||
- EMAIL_HOST=${EMAIL_HOST:-""}
|
||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
||||
- EMAIL_FROM=${EMAIL_FROM:-Team Plane <team@mailer.plane.so>}
|
||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
||||
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
||||
- AWS_REGION=${AWS_REGION:-""}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-}
|
||||
- GPT_ENGINE=${GPT_ENGINE:-gpt-3.5-turbo}
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||
- DOCKERIZED=${DOCKERIZED:-1}
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- NGINX_PORT=${NGINX_PORT:-80}
|
||||
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
|
||||
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
worker:
|
||||
container_name: bgworker
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/worker
|
||||
environment:
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- PGUSER=${PGUSER:-plane}
|
||||
- PGPASSWORD=${PGPASSWORD:-plane}
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}}
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
|
||||
- EMAIL_HOST=${EMAIL_HOST:-""}
|
||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
||||
- EMAIL_FROM=${EMAIL_FROM:-Team Plane <team@mailer.plane.so>}
|
||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
||||
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
||||
- AWS_REGION=${AWS_REGION:-""}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-}
|
||||
- GPT_ENGINE=${GPT_ENGINE:-gpt-3.5-turbo}
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||
- DOCKERIZED=${DOCKERIZED:-1}
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- NGINX_PORT=${NGINX_PORT:-80}
|
||||
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/beat
|
||||
environment:
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- PGUSER=${PGUSER:-plane}
|
||||
- PGPASSWORD=${PGPASSWORD:-plane}
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}}
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
|
||||
- EMAIL_HOST=${EMAIL_HOST:-""}
|
||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
||||
- EMAIL_FROM=${EMAIL_FROM:-Team Plane <team@mailer.plane.so>}
|
||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
||||
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
||||
- AWS_REGION=${AWS_REGION:-""}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-}
|
||||
- GPT_ENGINE=${GPT_ENGINE:-gpt-3.5-turbo}
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||
- DOCKERIZED=${DOCKERIZED:-1}
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- NGINX_PORT=${NGINX_PORT:-80}
|
||||
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
|
||||
plane-db:
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
restart: always
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:-plane}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-plane}
|
||||
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
|
||||
|
||||
plane-redis:
|
||||
container_name: plane-redis
|
||||
image: redis:6.2.7-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
plane-minio:
|
||||
container_name: plane-minio
|
||||
image: minio/minio
|
||||
restart: always
|
||||
command: server /export --console-address ":9090"
|
||||
volumes:
|
||||
- uploads:/export
|
||||
environment:
|
||||
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-access-key}
|
||||
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-secret-key}
|
||||
|
||||
createbuckets:
|
||||
image: minio/mc
|
||||
entrypoint: >
|
||||
/bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
depends_on:
|
||||
- plane-minio
|
||||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
proxy:
|
||||
container_name: proxy
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-proxy:latest
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
environment:
|
||||
- NGINX_PORT=${NGINX_PORT:-80}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
- BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
depends_on:
|
||||
- web
|
||||
- api
|
||||
- space
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
uploads:
|
||||
@@ -27,6 +27,7 @@
|
||||
"next-themes": "^0.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-moveable" : "^0.54.2",
|
||||
"@blueprintjs/popover2": "^2.0.10",
|
||||
"@tiptap/core": "^2.1.7",
|
||||
"@tiptap/extension-color": "^2.1.11",
|
||||
|
||||
@@ -16,143 +16,6 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
theme: {
|
||||
// scale down font sizes to 90% of default
|
||||
fontSize: {
|
||||
xs: "0.675rem",
|
||||
sm: "0.7875rem",
|
||||
base: "0.9rem",
|
||||
lg: "1.0125rem",
|
||||
xl: "1.125rem",
|
||||
"2xl": "1.35rem",
|
||||
"3xl": "1.6875rem",
|
||||
"4xl": "2.25rem",
|
||||
"5xl": "2.7rem",
|
||||
"6xl": "3.375rem",
|
||||
"7xl": "4.05rem",
|
||||
"8xl": "5.4rem",
|
||||
"9xl": "7.2rem",
|
||||
},
|
||||
// scale down spacing to 90% of default
|
||||
padding: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
margin: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
space: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
gap: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
extend: {
|
||||
boxShadow: {
|
||||
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
||||
@@ -350,6 +213,146 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
screens: {
|
||||
"3xl": "1792px",
|
||||
},
|
||||
// scale down font sizes to 90% of default
|
||||
fontSize: {
|
||||
xs: "0.675rem",
|
||||
sm: "0.7875rem",
|
||||
base: "0.9rem",
|
||||
lg: "1.0125rem",
|
||||
xl: "1.125rem",
|
||||
"2xl": "1.35rem",
|
||||
"3xl": "1.6875rem",
|
||||
"4xl": "2.25rem",
|
||||
"5xl": "2.7rem",
|
||||
"6xl": "3.375rem",
|
||||
"7xl": "4.05rem",
|
||||
"8xl": "5.4rem",
|
||||
"9xl": "7.2rem",
|
||||
},
|
||||
// scale down spacing to 90% of default
|
||||
padding: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
margin: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
space: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
gap: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
custom: ["Inter", "sans-serif"],
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-color" : "^3.0.9",
|
||||
"@types/node": "^20.5.2",
|
||||
"@types/react": "18.2.0",
|
||||
"@types/react-dom": "18.2.0",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { Children } from "react";
|
||||
|
||||
interface ICircularProgressIndicator {
|
||||
size: number;
|
||||
percentage: number;
|
||||
strokeWidth?: number;
|
||||
strokeColor?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CircularProgressIndicator: React.FC<ICircularProgressIndicator> = (
|
||||
props
|
||||
) => {
|
||||
const { size = 40, percentage = 25, strokeWidth = 6, children } = props;
|
||||
|
||||
const sqSize = size;
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const viewBox = `0 0 ${sqSize} ${sqSize}`;
|
||||
const dashArray = radius * Math.PI * 2;
|
||||
const dashOffset = dashArray - (dashArray * percentage) / 100;
|
||||
return (
|
||||
<div className="relative">
|
||||
<svg width={size} height={size} viewBox={viewBox} fill="none">
|
||||
<circle
|
||||
className="fill-none stroke-custom-background-80"
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
strokeWidth={`${strokeWidth}px`}
|
||||
style={{ filter: "url(#filter0_bi_377_19141)" }}
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_bi_377_19141"
|
||||
x="-3.57544"
|
||||
y="-3.57422"
|
||||
width="45.2227"
|
||||
height="45.2227"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="2" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_377_19141"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_377_19141"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="1" dy="1" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.63125 0 0 0 0 0.6625 0 0 0 0 0.75 0 0 0 0.35 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_377_19141"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
<circle
|
||||
className="stroke-custom-primary-100 fill-none "
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
strokeWidth={`${strokeWidth}px`}
|
||||
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||
style={{
|
||||
strokeDasharray: dashArray,
|
||||
strokeDashoffset: dashOffset,
|
||||
}}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./radial-progress";
|
||||
export * from "./progress-bar";
|
||||
export * from "./linear-progress-indicator";
|
||||
export * from "./circular-progress-indicator";
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { MoveRight } from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Icon } from "components/ui";
|
||||
// icons
|
||||
import { East } from "@mui/icons-material";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// store
|
||||
@@ -44,7 +40,7 @@ const peekModes: {
|
||||
];
|
||||
|
||||
export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, issueDetails } = props;
|
||||
const { handleClose } = props;
|
||||
|
||||
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
@@ -68,11 +64,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-4">
|
||||
{issueDetailStore.peekMode === "side" && (
|
||||
<button type="button" onClick={handleClose}>
|
||||
<East
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
/>
|
||||
<MoveRight className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
<Listbox
|
||||
|
||||
@@ -8,6 +8,9 @@ const nextConfig = {
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, "../"),
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
|
||||
+4
-2
@@ -7,7 +7,8 @@
|
||||
"develop": "next dev -p 4000",
|
||||
"build": "next build",
|
||||
"start": "next start -p 4000",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"export": "next export"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^4.16.3",
|
||||
@@ -15,8 +16,9 @@
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@headlessui/react": "^1.7.13",
|
||||
"@mui/icons-material": "^5.14.1",
|
||||
"@mui/material": "^5.14.1",
|
||||
"@plane/ui": "*",
|
||||
"@plane/lite-text-editor": "*",
|
||||
"@plane/rich-text-editor": "*",
|
||||
"axios": "^1.3.4",
|
||||
"clsx": "^2.0.0",
|
||||
|
||||
@@ -12,10 +12,20 @@ export interface EmailPasswordFormValues {
|
||||
|
||||
export interface IEmailPasswordForm {
|
||||
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
|
||||
buttonText?: string;
|
||||
submittingButtonText?: string;
|
||||
withForgetPassword?: boolean;
|
||||
withSignUpLink?: boolean;
|
||||
}
|
||||
|
||||
export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
|
||||
const { onSubmit } = props;
|
||||
const {
|
||||
onSubmit,
|
||||
buttonText = "Sign in",
|
||||
submittingButtonText = "Signing in...",
|
||||
withForgetPassword = false,
|
||||
withSignUpLink = false,
|
||||
} = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
// form info
|
||||
@@ -82,15 +92,17 @@ export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push("/accounts/forgot-password")}
|
||||
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||
>
|
||||
Forgot your password?
|
||||
</button>
|
||||
</div>
|
||||
{withForgetPassword && (
|
||||
<div className="text-right text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push("/accounts/forgot-password")}
|
||||
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||
>
|
||||
Forgot your password?
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -100,18 +112,20 @@ export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||
{isSubmitting ? submittingButtonText : buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push("/accounts/sign-up")}
|
||||
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||
>
|
||||
{"Don't have an account? Sign Up"}
|
||||
</button>
|
||||
</div>
|
||||
{withSignUpLink && (
|
||||
<div className="text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push("/accounts/sign-up")}
|
||||
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||
>
|
||||
{"Don't have an account? Sign Up"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// types
|
||||
type EmailPasswordFormValues = {
|
||||
export type EmailPasswordSignUpFormValues = {
|
||||
email: string;
|
||||
password?: string;
|
||||
confirm_password: string;
|
||||
@@ -12,7 +12,7 @@ type EmailPasswordFormValues = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
|
||||
onSubmit: (formData: EmailPasswordSignUpFormValues) => Promise<void>;
|
||||
};
|
||||
|
||||
export const EmailSignUpForm: React.FC<Props> = (props) => {
|
||||
@@ -23,7 +23,7 @@ export const EmailSignUpForm: React.FC<Props> = (props) => {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<EmailPasswordFormValues>({
|
||||
} = useForm<EmailPasswordSignUpFormValues>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
|
||||
@@ -8,7 +8,7 @@ import useUser from "hooks/use-user";
|
||||
// components
|
||||
import { CommandModal, ShortcutsModal } from "components/command-palette";
|
||||
import { BulkDeleteIssuesModal } from "components/core";
|
||||
import { CreateUpdateCycleModal } from "components/cycles";
|
||||
import { CycleCreateUpdateModal } from "components/cycles";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { CreateUpdateModuleModal } from "components/modules";
|
||||
import { CreateProjectModal } from "components/project";
|
||||
@@ -180,7 +180,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
)}
|
||||
{workspaceSlug && projectId && (
|
||||
<>
|
||||
<CreateUpdateCycleModal
|
||||
<CycleCreateUpdateModal
|
||||
isOpen={isCreateCycleModalOpen}
|
||||
handleClose={() => toggleCreateCycleModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
|
||||
@@ -15,58 +15,63 @@ type Props = {
|
||||
|
||||
export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEditLink, userAuth }) => {
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
return (
|
||||
<>
|
||||
{links.map((link) => (
|
||||
<div key={link.id} className="relative">
|
||||
{!isNotAllowed && (
|
||||
<div className="absolute top-1.5 right-1.5 z-[1] flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded bg-custom-background-90 p-1 outline-none hover:bg-custom-background-80"
|
||||
onClick={() => handleEditLink(link)}
|
||||
>
|
||||
<Pencil className="text-custom-text-200" />
|
||||
</button>
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="grid h-7 w-7 place-items-center rounded bg-custom-background-90 p-1 outline-none hover:bg-custom-background-80"
|
||||
>
|
||||
<ExternalLinkIcon className="h-4 w-4 text-custom-text-200" />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded bg-custom-background-90 p-1 text-red-500 outline-none duration-300 hover:bg-red-500/20"
|
||||
onClick={() => handleDeleteLink(link.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
|
||||
<div className="flex items-start justify-between gap-2 w-full">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="py-1">
|
||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
||||
</span>
|
||||
<span className="text-xs break-all">{link.title && link.title !== "" ? link.title : link.url}</span>
|
||||
</div>
|
||||
)}
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative flex gap-2 rounded-md bg-custom-background-90 p-2"
|
||||
>
|
||||
<div className="mt-0.5">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="w-4/5 break-words">{link.title ?? link.url}</h5>
|
||||
<p className="mt-0.5 text-custom-text-200">
|
||||
Added {timeAgo(link.created_at)}
|
||||
<br />
|
||||
by{" "}
|
||||
{link.created_by_detail.is_bot
|
||||
? link.created_by_detail.first_name + " Bot"
|
||||
: link.created_by_detail.display_name}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{!isNotAllowed && (
|
||||
<div className="flex items-center gap-2 flex-shrink-0 z-[1]">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleEditLink(link);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-3 w-3 text-custom-text-200 stroke-[1.5]" />
|
||||
</button>
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||
>
|
||||
<ExternalLinkIcon className="h-3 w-3 text-custom-text-200 stroke-[1.5]" />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDeleteLink(link.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<p className="text-xs mt-0.5 text-custom-text-300 stroke-[1.5]">
|
||||
Added {timeAgo(link.created_at)}
|
||||
<br />
|
||||
by{" "}
|
||||
{link.created_by_detail.is_bot
|
||||
? link.created_by_detail.first_name + " Bot"
|
||||
: link.created_by_detail.display_name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// images
|
||||
import emptyLabel from "public/empty-state/empty_label.svg";
|
||||
import emptyMembers from "public/empty-state/empty_members.svg";
|
||||
// components
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
import { SingleProgressStats } from "components/core";
|
||||
// ui
|
||||
import { Avatar } from "components/ui";
|
||||
@@ -17,9 +22,7 @@ import {
|
||||
TLabelsDistribution,
|
||||
TStateGroups,
|
||||
} from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
distribution: {
|
||||
assignees: TAssigneesDistribution[];
|
||||
@@ -33,6 +36,7 @@ type Props = {
|
||||
module?: IModule;
|
||||
roundedTab?: boolean;
|
||||
noBackground?: boolean;
|
||||
isPeekModuleDetails?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarProgressStats: React.FC<Props> = ({
|
||||
@@ -42,6 +46,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
module,
|
||||
roundedTab,
|
||||
noBackground,
|
||||
isPeekModuleDetails = false,
|
||||
}) => {
|
||||
const { filters, setFilters } = useIssuesView();
|
||||
|
||||
@@ -55,7 +60,6 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
return 1;
|
||||
case "States":
|
||||
return 2;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -72,7 +76,6 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
return setTab("Labels");
|
||||
case 2:
|
||||
return setTab("States");
|
||||
|
||||
default:
|
||||
return setTab("Assignees");
|
||||
}
|
||||
@@ -82,15 +85,17 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
as="div"
|
||||
className={`flex w-full items-center gap-2 justify-between rounded-md ${
|
||||
noBackground ? "" : "bg-custom-background-90"
|
||||
} px-1 py-1.5
|
||||
${module ? "text-xs" : "text-sm"} `}
|
||||
} p-0.5
|
||||
${module ? "text-xs" : "text-sm"}`}
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full ${
|
||||
roundedTab ? "rounded-3xl border border-custom-border-200" : "rounded"
|
||||
} px-3 py-1 text-custom-text-100 ${
|
||||
selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80"
|
||||
selected
|
||||
? "bg-custom-background-100 text-custom-text-300 shadow-custom-shadow-2xs"
|
||||
: "text-custom-text-400 hover:text-custom-text-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
@@ -101,7 +106,9 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
`w-full ${
|
||||
roundedTab ? "rounded-3xl border border-custom-border-200" : "rounded"
|
||||
} px-3 py-1 text-custom-text-100 ${
|
||||
selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80"
|
||||
selected
|
||||
? "bg-custom-background-100 text-custom-text-300 shadow-custom-shadow-2xs"
|
||||
: "text-custom-text-400 hover:text-custom-text-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
@@ -112,113 +119,128 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
`w-full ${
|
||||
roundedTab ? "rounded-3xl border border-custom-border-200" : "rounded"
|
||||
} px-3 py-1 text-custom-text-100 ${
|
||||
selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80"
|
||||
selected
|
||||
? "bg-custom-background-100 text-custom-text-300 shadow-custom-shadow-2xs"
|
||||
: "text-custom-text-400 hover:text-custom-text-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
States
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="flex w-full items-center justify-between pt-1 text-custom-text-200">
|
||||
<Tab.Panel as="div" className="w-full space-y-1">
|
||||
{distribution.assignees.map((assignee, index) => {
|
||||
if (assignee.assignee_id)
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={assignee.assignee_id}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
user={{
|
||||
id: assignee.assignee_id,
|
||||
avatar: assignee.avatar ?? "",
|
||||
first_name: assignee.first_name ?? "",
|
||||
last_name: assignee.last_name ?? "",
|
||||
display_name: assignee.display_name ?? "",
|
||||
}}
|
||||
/>
|
||||
<span>{assignee.display_name}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
total={assignee.total_issues}
|
||||
onClick={() => {
|
||||
if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
|
||||
setFilters({
|
||||
assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
|
||||
});
|
||||
else
|
||||
setFilters({
|
||||
assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
|
||||
});
|
||||
}}
|
||||
selected={filters?.assignees?.includes(assignee.assignee_id ?? "")}
|
||||
/>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={`unassigned-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img
|
||||
src="/user.png"
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="User"
|
||||
<Tab.Panels className="flex w-full items-center justify-between text-custom-text-200">
|
||||
<Tab.Panel as="div" className="flex flex-col gap-1.5 pt-3.5 w-full h-44 overflow-y-auto">
|
||||
{distribution.assignees.length > 0 ? (
|
||||
distribution.assignees.map((assignee, index) => {
|
||||
if (assignee.assignee_id)
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={assignee.assignee_id}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
user={{
|
||||
id: assignee.assignee_id,
|
||||
avatar: assignee.avatar ?? "",
|
||||
first_name: assignee.first_name ?? "",
|
||||
last_name: assignee.last_name ?? "",
|
||||
display_name: assignee.display_name ?? "",
|
||||
}}
|
||||
height="18px"
|
||||
width="18px"
|
||||
/>
|
||||
<span>{assignee.display_name}</span>
|
||||
</div>
|
||||
<span>No assignee</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
total={assignee.total_issues}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="w-full space-y-1">
|
||||
{distribution.labels.map((label, index) => (
|
||||
<SingleProgressStats
|
||||
key={label.label_id ?? `no-label-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
total={assignee.total_issues}
|
||||
{...(!isPeekModuleDetails && {
|
||||
onClick: () => {
|
||||
if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
|
||||
setFilters({
|
||||
assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
|
||||
});
|
||||
else
|
||||
setFilters({
|
||||
assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
|
||||
});
|
||||
},
|
||||
selected: filters?.assignees?.includes(assignee.assignee_id ?? ""),
|
||||
})}
|
||||
/>
|
||||
<span className="text-xs">{label.label_name ?? "No labels"}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed_issues}
|
||||
total={label.total_issues}
|
||||
onClick={() => {
|
||||
if (filters.labels?.includes(label.label_id ?? ""))
|
||||
setFilters({
|
||||
labels: filters?.labels?.filter((l) => l !== label.label_id),
|
||||
});
|
||||
else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
|
||||
}}
|
||||
selected={filters?.labels?.includes(label.label_id ?? "")}
|
||||
/>
|
||||
))}
|
||||
);
|
||||
else
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={`unassigned-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
|
||||
</div>
|
||||
<span>No assignee</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
total={assignee.total_issues}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-2 h-full">
|
||||
<div className="flex items-center justify-center h-20 w-20 bg-custom-background-80 rounded-full">
|
||||
<Image src={emptyMembers} className="h-12 w-12" alt="empty members" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">No assignees yet</h6>
|
||||
</div>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="w-full space-y-1">
|
||||
<Tab.Panel as="div" className="flex flex-col gap-1.5 pt-3.5 w-full h-44 overflow-y-auto">
|
||||
{distribution.labels.length > 0 ? (
|
||||
distribution.labels.map((label, index) => (
|
||||
<SingleProgressStats
|
||||
key={label.label_id ?? `no-label-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{label.label_name ?? "No labels"}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed_issues}
|
||||
total={label.total_issues}
|
||||
{...(!isPeekModuleDetails && {
|
||||
onClick: () => {
|
||||
if (filters.labels?.includes(label.label_id ?? ""))
|
||||
setFilters({
|
||||
labels: filters?.labels?.filter((l) => l !== label.label_id),
|
||||
});
|
||||
else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
|
||||
},
|
||||
selected: filters?.labels?.includes(label.label_id ?? ""),
|
||||
})}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-2 h-full">
|
||||
<div className="flex items-center justify-center h-20 w-20 bg-custom-background-80 rounded-full">
|
||||
<Image src={emptyLabel} className="h-12 w-12" alt="empty label" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">No labels yet</h6>
|
||||
</div>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="flex flex-col gap-1.5 pt-3.5 w-full h-44 overflow-y-auto">
|
||||
{Object.keys(groupedIssues).map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full "
|
||||
style={{
|
||||
backgroundColor: STATE_GROUP_COLORS[group as TStateGroups],
|
||||
}}
|
||||
/>
|
||||
<StateGroupIcon stateGroup={group as TStateGroups} />
|
||||
<span className="text-xs capitalize">{group}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { ProgressBar } from "@plane/ui";
|
||||
import { CircularProgressIndicator } from "@plane/ui";
|
||||
|
||||
type TSingleProgressStatsProps = {
|
||||
title: any;
|
||||
@@ -27,7 +27,7 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
||||
<div className="flex w-1/2 items-center justify-end gap-1 px-2">
|
||||
<div className="flex h-5 items-center justify-center gap-1">
|
||||
<span className="h-4 w-4">
|
||||
<ProgressBar value={completed} maxValue={total} />
|
||||
<CircularProgressIndicator percentage={(completed / total) * 100} size={14} strokeWidth={2} />
|
||||
</span>
|
||||
<span className="w-8 text-right">
|
||||
{isNaN(Math.floor((completed / total) * 100)) ? "0" : Math.floor((completed / total) * 100)}%
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { CycleForm } from "./form";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// types
|
||||
import { CycleDateCheckData, ICycle } from "types";
|
||||
|
||||
interface ICycleCreateEdit {
|
||||
cycle?: ICycle | null;
|
||||
modal: boolean;
|
||||
modalClose: () => void;
|
||||
onSubmit?: () => void;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const CycleCreateEditModal: React.FC<ICycleCreateEdit> = observer((props) => {
|
||||
const { modal, modalClose, cycle = null, onSubmit, workspaceSlug, projectId } = props;
|
||||
const [activeProject, setActiveProject] = useState<string | null>(null);
|
||||
|
||||
const { project: projectStore, cycle: cycleStore } = useMobxStore();
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const validateCycleDate = async (payload: CycleDateCheckData) => {
|
||||
let status = false;
|
||||
await cycleStore.validateDate(workspaceSlug as string, projectId as string, payload).then((res) => {
|
||||
status = res.status;
|
||||
});
|
||||
return status;
|
||||
};
|
||||
|
||||
const formSubmit = async (data: Partial<ICycle>) => {
|
||||
let isDateValid: boolean = true;
|
||||
|
||||
if (data?.start_date && data?.end_date) {
|
||||
if (cycle?.id && cycle?.start_date && cycle?.end_date)
|
||||
isDateValid = await validateCycleDate({
|
||||
start_date: data.start_date,
|
||||
end_date: data.end_date,
|
||||
cycle_id: cycle.id,
|
||||
});
|
||||
else
|
||||
isDateValid = await validateCycleDate({
|
||||
start_date: data.start_date,
|
||||
end_date: data.end_date,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDateValid)
|
||||
if (cycle) {
|
||||
try {
|
||||
await cycleStore.updateCycle(workspaceSlug, projectId, cycle.id, data);
|
||||
if (modalClose) modalClose();
|
||||
if (onSubmit) onSubmit();
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Cycle updated successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Warning!",
|
||||
message: "Something went wrong please try again later.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await cycleStore.createCycle(workspaceSlug, projectId, data);
|
||||
if (modalClose) modalClose();
|
||||
if (onSubmit) onSubmit();
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Cycle created successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Warning!",
|
||||
message: "Something went wrong please try again later.",
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "You already have a cycle on the given dates, if you want to create a draft cycle, remove the dates.",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// if modal is closed, reset active project to null
|
||||
// and return to avoid activeProject being set to some other project
|
||||
if (!modal) {
|
||||
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 (cycle && cycle.project) {
|
||||
setActiveProject(cycle.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, cycle, projectId, projects, modal]);
|
||||
|
||||
return (
|
||||
<Transition.Root show={modal} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={modalClose}>
|
||||
<Transition.Child
|
||||
as={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={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 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl p-5">
|
||||
<CycleForm
|
||||
handleFormSubmit={formSubmit}
|
||||
handleClose={modalClose}
|
||||
projectId={activeProject ?? ""}
|
||||
setActiveProject={setActiveProject}
|
||||
data={cycle}
|
||||
/>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
||||
@@ -7,8 +7,7 @@ import { Disclosure, Transition } from "@headlessui/react";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { SingleProgressStats } from "components/core";
|
||||
import { CycleCreateEditModal } from "./cycle-create-edit-modal";
|
||||
import { CycleDeleteModal } from "./cycle-delete-modal";
|
||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||
// ui
|
||||
import { AssigneesList } from "components/ui/avatar";
|
||||
import { CustomMenu, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui";
|
||||
@@ -69,19 +68,14 @@ export interface ICyclesBoardCard {
|
||||
|
||||
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
const { cycle, workspaceSlug, projectId } = props;
|
||||
|
||||
const [updateModal, setUpdateModal] = useState(false);
|
||||
const updateModalCallback = () => {};
|
||||
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const deleteModalCallback = () => {};
|
||||
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// states
|
||||
const [updateModal, setUpdateModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
// computed
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
const isCompleted = cycleStatus === "completed";
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
@@ -142,20 +136,18 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CycleCreateEditModal
|
||||
cycle={cycle}
|
||||
modal={updateModal}
|
||||
modalClose={() => setUpdateModal(false)}
|
||||
onSubmit={updateModalCallback}
|
||||
<CycleCreateUpdateModal
|
||||
data={cycle}
|
||||
isOpen={updateModal}
|
||||
handleClose={() => setUpdateModal(false)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<CycleDeleteModal
|
||||
cycle={cycle}
|
||||
modal={deleteModal}
|
||||
modalClose={() => setDeleteModal(false)}
|
||||
onSubmit={deleteModalCallback}
|
||||
isOpen={deleteModal}
|
||||
handleClose={() => setDeleteModal(false)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
@@ -3,8 +3,7 @@ import Link from "next/link";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CycleCreateEditModal } from "./cycle-create-edit-modal";
|
||||
import { CycleDeleteModal } from "./cycle-delete-modal";
|
||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||
// ui
|
||||
import { CustomMenu, RadialProgressBar, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui";
|
||||
// icons
|
||||
@@ -66,19 +65,14 @@ const stateGroups = [
|
||||
|
||||
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
const { cycle, workspaceSlug, projectId } = props;
|
||||
|
||||
const [updateModal, setUpdateModal] = useState(false);
|
||||
const updateModalCallback = () => {};
|
||||
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const deleteModalCallback = () => {};
|
||||
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// states
|
||||
const [updateModal, setUpdateModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
// computed
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
const isCompleted = cycleStatus === "completed";
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
@@ -347,20 +341,18 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CycleCreateEditModal
|
||||
cycle={cycle}
|
||||
modal={updateModal}
|
||||
modalClose={() => setUpdateModal(false)}
|
||||
onSubmit={updateModalCallback}
|
||||
<CycleCreateUpdateModal
|
||||
data={cycle}
|
||||
isOpen={updateModal}
|
||||
handleClose={() => setUpdateModal(false)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<CycleDeleteModal
|
||||
cycle={cycle}
|
||||
modal={deleteModal}
|
||||
modalClose={() => setDeleteModal(false)}
|
||||
onSubmit={deleteModalCallback}
|
||||
isOpen={deleteModal}
|
||||
handleClose={() => setDeleteModal(false)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
+11
-13
@@ -13,24 +13,23 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
interface ICycleDelete {
|
||||
cycle: ICycle;
|
||||
modal: boolean;
|
||||
modalClose: () => void;
|
||||
onSubmit?: () => void;
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||
const { modal, modalClose, cycle, onSubmit, workspaceSlug, projectId } = props;
|
||||
|
||||
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// states
|
||||
const [loader, setLoader] = useState(false);
|
||||
|
||||
const formSubmit = async () => {
|
||||
setLoader(true);
|
||||
|
||||
if (cycle?.id)
|
||||
try {
|
||||
await cycleStore.removeCycle(workspaceSlug, projectId, cycle?.id);
|
||||
@@ -39,8 +38,7 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||
title: "Success!",
|
||||
message: "Cycle deleted successfully.",
|
||||
});
|
||||
if (modalClose) modalClose();
|
||||
if (onSubmit) onSubmit();
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@@ -61,8 +59,8 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Transition.Root show={modal} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={modalClose}>
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@@ -103,7 +101,7 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||
</p>
|
||||
</span>
|
||||
<div className="flex justify-end gap-2">
|
||||
<SecondaryButton onClick={modalClose}>Cancel</SecondaryButton>
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<DangerButton onClick={formSubmit} loading={loader}>
|
||||
{loader ? "Deleting..." : "Delete Cycle"}
|
||||
</DangerButton>
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
|
||||
handleClose: () => void;
|
||||
projectId: string;
|
||||
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setActiveProject: (projectId: string) => void;
|
||||
data?: ICycle | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,3 +16,4 @@ export * from "./cycles-list-item";
|
||||
export * from "./cycles-board";
|
||||
export * from "./cycles-board-card";
|
||||
export * from "./cycles-gantt";
|
||||
export * from "./delete-modal";
|
||||
|
||||
+17
-113
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { mutate } from "swr";
|
||||
import React, { useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { CycleService } from "services/cycle.service";
|
||||
@@ -8,20 +7,8 @@ import useToast from "hooks/use-toast";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CycleForm } from "components/cycles";
|
||||
// helper
|
||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { CycleDateCheckData, ICycle, IProject, IUser } from "types";
|
||||
// fetch keys
|
||||
import {
|
||||
COMPLETED_CYCLES_LIST,
|
||||
CURRENT_CYCLE_LIST,
|
||||
CYCLES_LIST,
|
||||
DRAFT_CYCLES_LIST,
|
||||
INCOMPLETE_CYCLES_LIST,
|
||||
PROJECT_DETAILS,
|
||||
UPCOMING_CYCLES_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import type { CycleDateCheckData, ICycle } from "types";
|
||||
|
||||
type CycleModalProps = {
|
||||
isOpen: boolean;
|
||||
@@ -34,49 +21,19 @@ type CycleModalProps = {
|
||||
// services
|
||||
const cycleService = new CycleService();
|
||||
|
||||
export const CreateUpdateCycleModal: React.FC<CycleModalProps> = (props) => {
|
||||
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
|
||||
const [activeProject, setActiveProject] = useState<string | null>(null);
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
// states
|
||||
const [activeProject, setActiveProject] = useState<string>(projectId);
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const createCycle = async (payload: Partial<ICycle>) => {
|
||||
await cycleService
|
||||
.createCycle(workspaceSlug.toString(), projectId.toString(), payload, {} as IUser)
|
||||
.then((res) => {
|
||||
switch (getDateRangeStatus(res.start_date, res.end_date)) {
|
||||
case "completed":
|
||||
mutate(COMPLETED_CYCLES_LIST(projectId.toString()));
|
||||
break;
|
||||
case "current":
|
||||
mutate(CURRENT_CYCLE_LIST(projectId.toString()));
|
||||
break;
|
||||
case "upcoming":
|
||||
mutate(UPCOMING_CYCLES_LIST(projectId.toString()));
|
||||
break;
|
||||
default:
|
||||
mutate(DRAFT_CYCLES_LIST(projectId.toString()));
|
||||
}
|
||||
mutate(INCOMPLETE_CYCLES_LIST(projectId.toString()));
|
||||
mutate(CYCLES_LIST(projectId.toString()));
|
||||
|
||||
// update total cycles count in the project details
|
||||
mutate<IProject>(
|
||||
PROJECT_DETAILS(projectId.toString()),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
total_cycles: prevData.total_cycles + 1,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const createCycle = async (payload: Partial<ICycle>) =>
|
||||
cycleStore
|
||||
.createCycle(workspaceSlug, projectId, payload)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
@@ -90,42 +47,11 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = (props) => {
|
||||
message: "Error in creating cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
|
||||
await cycleService
|
||||
.updateCycle(workspaceSlug.toString(), projectId.toString(), cycleId, payload, {} as IUser)
|
||||
.then((res) => {
|
||||
switch (getDateRangeStatus(data?.start_date, data?.end_date)) {
|
||||
case "completed":
|
||||
mutate(COMPLETED_CYCLES_LIST(projectId.toString()));
|
||||
break;
|
||||
case "current":
|
||||
mutate(CURRENT_CYCLE_LIST(projectId.toString()));
|
||||
break;
|
||||
case "upcoming":
|
||||
mutate(UPCOMING_CYCLES_LIST(projectId.toString()));
|
||||
break;
|
||||
default:
|
||||
mutate(DRAFT_CYCLES_LIST(projectId.toString()));
|
||||
}
|
||||
mutate(CYCLES_LIST(projectId.toString()));
|
||||
if (getDateRangeStatus(data?.start_date, data?.end_date) != getDateRangeStatus(res.start_date, res.end_date)) {
|
||||
switch (getDateRangeStatus(res.start_date, res.end_date)) {
|
||||
case "completed":
|
||||
mutate(COMPLETED_CYCLES_LIST(projectId.toString()));
|
||||
break;
|
||||
case "current":
|
||||
mutate(CURRENT_CYCLE_LIST(projectId.toString()));
|
||||
break;
|
||||
case "upcoming":
|
||||
mutate(UPCOMING_CYCLES_LIST(projectId.toString()));
|
||||
break;
|
||||
default:
|
||||
mutate(DRAFT_CYCLES_LIST(projectId.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) =>
|
||||
cycleStore
|
||||
.updateCycle(workspaceSlug, projectId, cycleId, payload)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
@@ -139,7 +65,6 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = (props) => {
|
||||
message: "Error in updating cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const dateChecker = async (payload: CycleDateCheckData) => {
|
||||
let status = false;
|
||||
@@ -186,27 +111,6 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
@@ -237,7 +141,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = (props) => {
|
||||
<CycleForm
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
handleClose={handleClose}
|
||||
projectId={activeProject ?? ""}
|
||||
projectId={activeProject}
|
||||
setActiveProject={setActiveProject}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { ContrastIcon } from "@plane/ui";
|
||||
@@ -9,7 +8,7 @@ import { Plus } from "lucide-react";
|
||||
// services
|
||||
import { CycleService } from "services/cycle.service";
|
||||
// components
|
||||
import { CreateUpdateCycleModal } from "components/cycles";
|
||||
import { CycleCreateUpdateModal } from "components/cycles";
|
||||
// fetch-keys
|
||||
import { CYCLES_LIST } from "constants/fetch-keys";
|
||||
|
||||
@@ -29,8 +28,6 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({ projectId, value,
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { data: cycles } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLES_LIST(projectId) : null,
|
||||
workspaceSlug && projectId
|
||||
@@ -51,7 +48,7 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({ projectId, value,
|
||||
return (
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
<CreateUpdateCycleModal
|
||||
<CycleCreateUpdateModal
|
||||
isOpen={isCycleModalActive}
|
||||
handleClose={closeCycleModal}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
|
||||
@@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { SidebarProgressStats } from "components/core";
|
||||
import ProgressChart from "components/core/sidebar/progress-chart";
|
||||
import { CycleDeleteModal } from "components/cycles/cycle-delete-modal";
|
||||
import { CycleDeleteModal } from "components/cycles/delete-modal";
|
||||
// ui
|
||||
import { CustomRangeDatePicker } from "components/ui";
|
||||
import { CustomMenu, Loader, ProgressBar } from "@plane/ui";
|
||||
@@ -285,17 +285,16 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
{cycleDetails && workspaceSlug && projectId && (
|
||||
<CycleDeleteModal
|
||||
cycle={cycleDetails}
|
||||
modal={cycleDeleteModal}
|
||||
modalClose={() => setCycleDeleteModal(false)}
|
||||
onSubmit={() => {}}
|
||||
isOpen={cycleDeleteModal}
|
||||
handleClose={() => setCycleDeleteModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`absolute top-0 z-20 ${
|
||||
className={`fixed top-[66px] ${
|
||||
isOpen ? "right-0" : "-right-[24rem]"
|
||||
} h-full w-[24rem] overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 pt-5 pb-10 duration-300`}
|
||||
} h-full w-[24rem] overflow-y-auto border-l border-custom-border-200 bg-custom-sidebar-background-100 pt-5 pb-10 duration-300`}
|
||||
>
|
||||
{cycleDetails ? (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import React, { useState } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Check, ChevronDown, Search, Triangle } from "lucide-react";
|
||||
// types
|
||||
import { Tooltip } from "components/ui";
|
||||
import { Placement } from "@popperjs/core";
|
||||
// constants
|
||||
import { IEstimatePoint } from "types";
|
||||
|
||||
type Props = {
|
||||
value: number | null;
|
||||
onChange: (value: number | null) => void;
|
||||
estimatePoints: IEstimatePoint[] | undefined;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
placement?: Placement;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const EstimateSelect: React.FC<Props> = (props) => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
estimatePoints,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
placement,
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
} = props;
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const options: { value: number | null; query: string; content: any }[] | undefined = estimatePoints?.map(
|
||||
(estimate) => ({
|
||||
value: estimate.key,
|
||||
query: estimate.value,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Triangle className="h-3 w-3" strokeWidth={2} />
|
||||
{estimate.value}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
options?.unshift({
|
||||
value: null,
|
||||
query: "none",
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Triangle className="h-3 w-3" strokeWidth={2} />
|
||||
None
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
const filteredOptions =
|
||||
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const selectedEstimate = estimatePoints?.find((e) => e.key === value);
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="Estimate" tooltipContent={selectedEstimate?.value ?? "None"} position="top">
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
<Triangle className="h-3 w-3" strokeWidth={2} />
|
||||
<span className="truncate">{selectedEstimate?.value ?? "None"}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value}
|
||||
onChange={(val) => onChange(val as number | null)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Combobox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded border-[0.5px] border-custom-border-300 duration-300 focus:outline-none ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<div
|
||||
className={`border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <Check className="h-3.5 w-3.5" />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./create-update-estimate-modal";
|
||||
export * from "./single-estimate";
|
||||
export * from "./delete-estimate-modal";
|
||||
export * from "./estimate-select";
|
||||
export * from "./single-estimate";
|
||||
@@ -1,6 +1,4 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
// components
|
||||
import { GanttChartBlocks } from "components/gantt-chart";
|
||||
@@ -13,7 +11,7 @@ import { MonthChartView } from "./month";
|
||||
// import { QuarterChartView } from "./quarter";
|
||||
// import { YearChartView } from "./year";
|
||||
// icons
|
||||
import { Expand, PlusIcon, Shrink } from "lucide-react";
|
||||
import { Expand, Shrink } from "lucide-react";
|
||||
// views
|
||||
import {
|
||||
// generateHourChart,
|
||||
@@ -28,7 +26,6 @@ import {
|
||||
// getNumberOfDaysBetweenTwoDatesInYear,
|
||||
getMonthChartItemPositionWidthInMonth,
|
||||
} from "../views";
|
||||
// import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form";
|
||||
// types
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
|
||||
// data
|
||||
@@ -65,15 +62,9 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
||||
enableReorder,
|
||||
bottomSpacing,
|
||||
}) => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
const isCyclePage = router.pathname.split("/")[4] === "cycles" && !cycleId;
|
||||
const isModulePage = router.pathname.split("/")[4] === "modules" && !moduleId;
|
||||
// states
|
||||
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
|
||||
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); // blocks state management starts
|
||||
// hooks
|
||||
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart();
|
||||
@@ -297,44 +288,6 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
||||
SidebarBlockRender={SidebarBlockRender}
|
||||
enableReorder={enableReorder}
|
||||
/>
|
||||
{chartBlocks && !(isCyclePage || isModulePage) && (
|
||||
<div className="pl-2.5 py-3">
|
||||
{/* <GanttInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
onSuccess={() => {
|
||||
const ganttSidebar = document.getElementById(`gantt-sidebar-${cycleId}`);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (ganttSidebar)
|
||||
ganttSidebar.scrollBy({
|
||||
top: ganttSidebar.scrollHeight,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
}, 10);
|
||||
}}
|
||||
prePopulatedData={{
|
||||
start_date: new Date(Date.now()).toISOString().split("T")[0],
|
||||
target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0],
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/> */}
|
||||
|
||||
{!isCreateIssueFormOpen && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 pl-[1.875rem] py-1 rounded-md"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto horizontal-scroll-enable"
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
// hooks
|
||||
import { useChart } from "./hooks";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// helpers
|
||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IBlockUpdateData, IGanttBlock } from "./types";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blocks: IGanttBlock[] | null;
|
||||
SidebarBlockRender: React.FC<any>;
|
||||
enableReorder: boolean;
|
||||
};
|
||||
|
||||
export const GanttSidebar: React.FC<Props> = (props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query;
|
||||
|
||||
const { activeBlock, dispatch } = useChart();
|
||||
|
||||
// update the active block on hover
|
||||
const updateActiveBlock = (block: IGanttBlock | null) => {
|
||||
dispatch({
|
||||
type: "PARTIAL_UPDATE",
|
||||
payload: {
|
||||
activeBlock: block,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOrderChange = (result: DropResult) => {
|
||||
if (!blocks) return;
|
||||
|
||||
const { source, destination } = result;
|
||||
|
||||
// return if dropped outside the list
|
||||
if (!destination) return;
|
||||
|
||||
// return if dropped on the same index
|
||||
if (source.index === destination.index) return;
|
||||
|
||||
let updatedSortOrder = blocks[source.index].sort_order;
|
||||
|
||||
// update the sort order to the lowest if dropped at the top
|
||||
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
||||
// update the sort order to the highest if dropped at the bottom
|
||||
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
|
||||
// update the sort order to the average of the two adjacent blocks if dropped in between
|
||||
else {
|
||||
const destinationSortingOrder = blocks[destination.index].sort_order;
|
||||
const relativeDestinationSortingOrder =
|
||||
source.index < destination.index
|
||||
? blocks[destination.index + 1].sort_order
|
||||
: blocks[destination.index - 1].sort_order;
|
||||
|
||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
||||
}
|
||||
|
||||
// extract the element from the source index and insert it at the destination index without updating the entire array
|
||||
const removedElement = blocks.splice(source.index, 1)[0];
|
||||
blocks.splice(destination.index, 0, removedElement);
|
||||
|
||||
// call the block update handler with the updated sort order, new and old index
|
||||
blockUpdateHandler(removedElement.data, {
|
||||
sort_order: {
|
||||
destinationIndex: destination.index,
|
||||
newSortOrder: updatedSortOrder,
|
||||
sourceIndex: source.index,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleOrderChange}>
|
||||
<StrictModeDroppable droppableId="gantt-sidebar">
|
||||
{(droppableProvided) => (
|
||||
<div
|
||||
id={`gantt-sidebar-${cycleId}`}
|
||||
className="max-h-full overflow-y-auto pl-2.5 mt-3"
|
||||
ref={droppableProvided.innerRef}
|
||||
{...droppableProvided.droppableProps}
|
||||
>
|
||||
<>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? "", true);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={`sidebar-block-${block.id}`}
|
||||
draggableId={`sidebar-block-${block.id}`}
|
||||
index={index}
|
||||
isDragDisabled={!enableReorder}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`h-11 ${snapshot.isDragging ? "bg-custom-background-80 rounded" : ""}`}
|
||||
onMouseEnter={() => updateActiveBlock(block)}
|
||||
onMouseLeave={() => updateActiveBlock(null)}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<div
|
||||
id={`sidebar-block-${block.id}`}
|
||||
className={`group h-full w-full flex items-center gap-2 rounded-l px-2 pr-4 ${
|
||||
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
>
|
||||
{enableReorder && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded p-0.5 text-custom-sidebar-text-200 flex flex-shrink-0 opacity-0 group-hover:opacity-100"
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<MoreVertical className="h-3.5 w-3.5" />
|
||||
<MoreVertical className="h-3.5 w-3.5 -ml-5" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex-grow truncate h-full flex items-center justify-between gap-2">
|
||||
<div className="flex-grow truncate">
|
||||
<SidebarBlockRender data={block.data} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Loader className="pr-2 space-y-3">
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
</Loader>
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
// hooks
|
||||
import { useChart } from "./hooks";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// helpers
|
||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IBlockUpdateData, IGanttBlock } from "./types";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blocks: IGanttBlock[] | null;
|
||||
SidebarBlockRender: React.FC<any>;
|
||||
enableReorder: boolean;
|
||||
};
|
||||
|
||||
export const GanttSidebar: React.FC<Props> = (props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query;
|
||||
|
||||
const { activeBlock, dispatch } = useChart();
|
||||
|
||||
// update the active block on hover
|
||||
const updateActiveBlock = (block: IGanttBlock | null) => {
|
||||
dispatch({
|
||||
type: "PARTIAL_UPDATE",
|
||||
payload: {
|
||||
activeBlock: block,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOrderChange = (result: DropResult) => {
|
||||
if (!blocks) return;
|
||||
|
||||
const { source, destination } = result;
|
||||
|
||||
// return if dropped outside the list
|
||||
if (!destination) return;
|
||||
|
||||
// return if dropped on the same index
|
||||
if (source.index === destination.index) return;
|
||||
|
||||
let updatedSortOrder = blocks[source.index].sort_order;
|
||||
|
||||
// update the sort order to the lowest if dropped at the top
|
||||
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
||||
// update the sort order to the highest if dropped at the bottom
|
||||
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
|
||||
// update the sort order to the average of the two adjacent blocks if dropped in between
|
||||
else {
|
||||
const destinationSortingOrder = blocks[destination.index].sort_order;
|
||||
const relativeDestinationSortingOrder =
|
||||
source.index < destination.index
|
||||
? blocks[destination.index + 1].sort_order
|
||||
: blocks[destination.index - 1].sort_order;
|
||||
|
||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
||||
}
|
||||
|
||||
// extract the element from the source index and insert it at the destination index without updating the entire array
|
||||
const removedElement = blocks.splice(source.index, 1)[0];
|
||||
blocks.splice(destination.index, 0, removedElement);
|
||||
|
||||
// call the block update handler with the updated sort order, new and old index
|
||||
blockUpdateHandler(removedElement.data, {
|
||||
sort_order: {
|
||||
destinationIndex: destination.index,
|
||||
newSortOrder: updatedSortOrder,
|
||||
sourceIndex: source.index,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleOrderChange}>
|
||||
<StrictModeDroppable droppableId="gantt-sidebar">
|
||||
{(droppableProvided) => (
|
||||
<div
|
||||
id={`gantt-sidebar-${cycleId}`}
|
||||
className="max-h-full overflow-y-auto pl-2.5 mt-3"
|
||||
ref={droppableProvided.innerRef}
|
||||
{...droppableProvided.droppableProps}
|
||||
>
|
||||
<>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? "", true);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={`sidebar-block-${block.id}`}
|
||||
draggableId={`sidebar-block-${block.id}`}
|
||||
index={index}
|
||||
isDragDisabled={!enableReorder}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`h-11 ${snapshot.isDragging ? "bg-custom-background-80 rounded" : ""}`}
|
||||
onMouseEnter={() => updateActiveBlock(block)}
|
||||
onMouseLeave={() => updateActiveBlock(null)}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<div
|
||||
id={`sidebar-block-${block.id}`}
|
||||
className={`group h-full w-full flex items-center gap-2 rounded-l px-2 pr-4 ${
|
||||
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
>
|
||||
{enableReorder && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded p-0.5 text-custom-sidebar-text-200 flex flex-shrink-0 opacity-0 group-hover:opacity-100"
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<MoreVertical className="h-3.5 w-3.5" />
|
||||
<MoreVertical className="h-3.5 w-3.5 -ml-5" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex-grow truncate h-full flex items-center justify-between gap-2">
|
||||
<div className="flex-grow truncate">
|
||||
<SidebarBlockRender data={block.data} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Loader className="pr-2 space-y-3">
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
</Loader>
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,8 @@ import { MoreVertical } from "lucide-react";
|
||||
import { useChart } from "./hooks";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { GanttInlineCreateIssueForm } from "components/issues";
|
||||
// helpers
|
||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||
// types
|
||||
@@ -17,11 +19,12 @@ type Props = {
|
||||
blocks: IGanttBlock[] | null;
|
||||
SidebarBlockRender: React.FC<any>;
|
||||
enableReorder: boolean;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
};
|
||||
|
||||
export const GanttSidebar: React.FC<Props> = (props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props;
|
||||
const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder, enableQuickIssueCreate } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query;
|
||||
@@ -150,6 +153,7 @@ export const GanttSidebar: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</>
|
||||
<GanttInlineCreateIssueForm />
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, Plus } from "lucide-react";
|
||||
import { Plus } from "lucide-react";
|
||||
// ui
|
||||
import { Breadcrumbs, BreadcrumbItem, Button } from "@plane/ui";
|
||||
// helpers
|
||||
@@ -23,15 +23,6 @@ export const CyclesHeader: React.FC<ICyclesHeader> = (props) => {
|
||||
className={`relative z-10 flex w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
|
||||
>
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<div className="block md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft fontSize={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
|
||||
@@ -10,6 +10,12 @@ export * from "./workspace-dashboard";
|
||||
export * from "./projects";
|
||||
export * from "./profile-preferences";
|
||||
export * from "./cycles";
|
||||
export * from "./modules";
|
||||
export * from "./modules-list";
|
||||
export * from "./project-settings";
|
||||
export * from "./workspace-settings";
|
||||
export * from "./pages";
|
||||
export * from "./project-draft-issues";
|
||||
export * from "./project-archived-issue-details";
|
||||
export * from "./project-archived-issues";
|
||||
export * from "./project-issue-details";
|
||||
export * from "./user-profile";
|
||||
|
||||
@@ -1,51 +1,46 @@
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, Plus } from "lucide-react";
|
||||
// components
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { GanttChart, LayoutGrid, List, Plus } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// ui
|
||||
import { Breadcrumbs, BreadcrumbItem, Button, Tooltip } from "@plane/ui";
|
||||
import { Icon } from "components/ui";
|
||||
// helper
|
||||
import { replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper";
|
||||
|
||||
export interface IModulesHeader {
|
||||
name: string | undefined;
|
||||
modulesView: string;
|
||||
setModulesView: Dispatch<SetStateAction<"grid" | "gantt_chart">>;
|
||||
}
|
||||
|
||||
const moduleViewOptions: { type: "grid" | "gantt_chart"; icon: any }[] = [
|
||||
const moduleViewOptions: { type: "list" | "grid" | "gantt_chart"; icon: any }[] = [
|
||||
{
|
||||
type: "gantt_chart",
|
||||
icon: "view_timeline",
|
||||
type: "list",
|
||||
icon: List,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
icon: "table_rows",
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
{
|
||||
type: "gantt_chart",
|
||||
icon: GanttChart,
|
||||
},
|
||||
];
|
||||
|
||||
export const ModulesHeader: FC<IModulesHeader> = (props) => {
|
||||
const { name, modulesView, setModulesView } = props;
|
||||
export const ModulesListHeader: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined;
|
||||
|
||||
const { storedValue: modulesView, setValue: setModulesView } = useLocalStorage("modules_view", "grid");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative z-10 flex w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
|
||||
>
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<div className="block md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft fontSize={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
@@ -57,7 +52,7 @@ export const ModulesHeader: FC<IModulesHeader> = (props) => {
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BreadcrumbItem title={`${truncateText(name ?? "Project", 32)} Modules`} />
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Modules`} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +70,7 @@ export const ModulesHeader: FC<IModulesHeader> = (props) => {
|
||||
}`}
|
||||
onClick={() => setModulesView(option.type)}
|
||||
>
|
||||
<Icon iconName={option.icon} className={`!text-base ${option.type === "grid" ? "rotate-90" : ""}`} />
|
||||
<option.icon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
@@ -92,4 +87,4 @@ export const ModulesHeader: FC<IModulesHeader> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Breadcrumbs, BreadcrumbItem, Button } from "@plane/ui";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
export interface IPagesHeaderProps {
|
||||
showButton?: boolean;
|
||||
}
|
||||
|
||||
export const PagesHeader: FC<IPagesHeaderProps> = observer((props) => {
|
||||
const { showButton = false } = props;
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}/projects`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p>Projects</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Pages`} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
{showButton && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
prependIcon={<Plus />}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "d" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
Create Page
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
// components
|
||||
import { BreadcrumbItem, Breadcrumbs } from "@plane/ui";
|
||||
|
||||
@@ -11,15 +10,6 @@ export const ProfilePreferencesHeader = () => {
|
||||
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div className="block md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft fontSize={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem title="My Profile Preferences" />
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { FC } from "react";
|
||||
import useSWR from "swr";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Breadcrumbs, BreadcrumbItem } from "@plane/ui";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
// services
|
||||
import { IssueArchiveService } from "services/issue";
|
||||
|
||||
const issueArchiveService = new IssueArchiveService();
|
||||
|
||||
export const ProjectArchivedIssueDetailsHeader: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, archivedIssueId } = router.query;
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||
: undefined;
|
||||
|
||||
const { data: issueDetails } = useSWR<IIssue | undefined>(
|
||||
workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId as string) : null,
|
||||
workspaceSlug && projectId && archivedIssueId
|
||||
? () =>
|
||||
issueArchiveService.retrieveArchivedIssue(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
archivedIssueId as string
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId as string}/issues`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p className="truncate">{`${truncateText(
|
||||
issueDetails?.project_detail.name ?? "Project",
|
||||
32
|
||||
)} Issues`}</p>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Archived Issues`} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Breadcrumbs, BreadcrumbItem } from "@plane/ui";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
export const ProjectArchivedIssuesHeader: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<Link href={`/${workspaceSlug}/projects`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p>Projects</p>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Archived Issues`} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Breadcrumbs, BreadcrumbItem } from "@plane/ui";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}/projects`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p>Projects</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Draft Issues`} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,21 +1,54 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Breadcrumbs, BreadcrumbItem, Button } from "@plane/ui";
|
||||
// components
|
||||
import { CreateInboxIssueModal } from "components/inbox";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { Plus } from "lucide-react";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
export const ProjectInboxHeader = () => {
|
||||
export const ProjectInboxHeader: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateInboxIssueModal isOpen={createIssueModal} onClose={() => setCreateIssueModal(false)} />
|
||||
<Button onClick={() => setCreateIssueModal(true)} size="sm" prependIcon={<Plus />}>
|
||||
Add Issue
|
||||
</Button>
|
||||
</>
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}/projects`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p>Projects</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Inbox`} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<CreateInboxIssueModal isOpen={createIssueModal} onClose={() => setCreateIssueModal(false)} />
|
||||
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => setCreateIssueModal(true)}>
|
||||
Add Issue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FC } from "react";
|
||||
import useSWR from "swr";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// ui
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
// constants
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
// services
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const ProjectIssueDetailsHeader: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { data: issueDetails } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId as string}/issues`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p className="truncate">{`${truncateText(
|
||||
issueDetails?.project_detail.name ?? "Project",
|
||||
32
|
||||
)} Issues`}</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
title={`Issue ${issueDetails?.project_detail.identifier ?? "Project"}-${
|
||||
issueDetails?.sequence_id ?? "..."
|
||||
} Details`}
|
||||
unshrinkTitle
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useState, FC } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Plus } from "lucide-react";
|
||||
// hooks
|
||||
import { ArrowLeft, Circle, ExternalLink, Plus } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
@@ -17,7 +17,7 @@ import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
export const ProjectIssuesHeader: FC = observer(() => {
|
||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
@@ -93,6 +93,8 @@ export const ProjectIssuesHeader: FC = observer(() => {
|
||||
|
||||
const inboxDetails = projectId ? inboxStore.inboxesList?.[projectId.toString()]?.[0] : undefined;
|
||||
|
||||
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -125,6 +127,18 @@ export const ProjectIssuesHeader: FC = observer(() => {
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
{projectDetails?.is_deployed && deployUrl && (
|
||||
<a
|
||||
href={`${deployUrl}/${workspaceSlug}/${projectDetails?.id}`}
|
||||
className="group bg-custom-primary-100/20 text-custom-primary-100 px-2.5 py-1 text-xs flex items-center gap-1.5 rounded font-medium"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Circle className="h-1.5 w-1.5 fill-custom-primary-100" strokeWidth={2} />
|
||||
Public
|
||||
<ExternalLink className="h-3 w-3 hidden group-hover:block" strokeWidth={2} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
|
||||
@@ -2,7 +2,6 @@ import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "@plane/ui";
|
||||
// helper
|
||||
@@ -28,15 +27,6 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
|
||||
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div className="block md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft fontSize={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
// constants
|
||||
@@ -19,6 +23,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
issueFilter: issueFilterStore,
|
||||
projectViewFilters: projectViewFiltersStore,
|
||||
project: projectStore,
|
||||
projectViews: projectViewsStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
|
||||
@@ -82,32 +87,78 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||
: undefined;
|
||||
|
||||
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
|
||||
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={storedFilters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
<div className="relative w-full flex items-center z-10 justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}/projects/${projectDetails?.id}/cycles`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p className="truncate">{`${projectDetails?.name ?? "Project"} Views`}</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<PhotoFilterIcon height={12} width={12} />
|
||||
{viewDetails?.name && truncateText(viewDetails.name, 40)}
|
||||
</>
|
||||
}
|
||||
className="ml-1.5"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{viewsList?.map((view) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={view.id}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/views/${view.id}`)}
|
||||
>
|
||||
{truncateText(view.name, 40)}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={storedFilters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { FC, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { ArrowLeft, Link, Plus } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CreateUpdateProjectViewModal } from "components/views";
|
||||
// components
|
||||
@@ -11,18 +14,20 @@ import { PrimaryButton } from "components/ui";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
interface IProjectViewsHeader {
|
||||
title: string | undefined;
|
||||
}
|
||||
|
||||
export const ProjectViewsHeader: FC<IProjectViewsHeader> = (props) => {
|
||||
const { title } = props;
|
||||
export const ProjectViewsHeader: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// states
|
||||
const [createViewModal, setCreateViewModal] = useState(false);
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
@@ -37,15 +42,6 @@ export const ProjectViewsHeader: FC<IProjectViewsHeader> = (props) => {
|
||||
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div className="block md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft fontSize={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
@@ -57,20 +53,13 @@ export const ProjectViewsHeader: FC<IProjectViewsHeader> = (props) => {
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BreadcrumbItem title={`${truncateText(title ?? "Project", 32)} Cycles`} />
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Views`} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div>
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "v" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<PrimaryButton type="button" className="flex items-center gap-2" onClick={() => setCreateViewModal(true)}>
|
||||
<Plus size={14} strokeWidth={2} />
|
||||
Create View
|
||||
</PrimaryButton>
|
||||
@@ -79,4 +68,4 @@ export const ProjectViewsHeader: FC<IProjectViewsHeader> = (props) => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { ArrowLeft, Search, Plus } from "lucide-react";
|
||||
import { Search, Plus } from "lucide-react";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs, Button } from "@plane/ui";
|
||||
// helper
|
||||
@@ -19,15 +19,6 @@ export const ProjectsHeader = observer(() => {
|
||||
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div className="block md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft fontSize={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FC } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "@plane/ui";
|
||||
// hooks
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
export const UserProfileHeader: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div
|
||||
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem title="User Profile" unshrinkTitle />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import useSWR from "swr";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "@plane/ui";
|
||||
// hooks
|
||||
@@ -37,15 +36,6 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
|
||||
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div className="block md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft fontSize={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
|
||||
@@ -40,15 +40,27 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
<CalendarHeader />
|
||||
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
{layout === "month" ? (
|
||||
{layout === "month" && (
|
||||
<div className="h-full w-full grid grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
|
||||
{allWeeksOfActiveMonth &&
|
||||
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
||||
<CalendarWeekDays key={weekIndex} week={week} issues={issues} quickActions={quickActions} />
|
||||
<CalendarWeekDays
|
||||
key={weekIndex}
|
||||
week={week}
|
||||
issues={issues}
|
||||
enableQuickIssueCreate
|
||||
quickActions={quickActions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} quickActions={quickActions} />
|
||||
)}
|
||||
{layout === "week" && (
|
||||
<CalendarWeekDays
|
||||
week={calendarStore.allDaysOfActiveWeek}
|
||||
issues={issues}
|
||||
enableQuickIssueCreate
|
||||
quickActions={quickActions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Droppable } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarIssueBlocks, ICalendarDate } from "components/issues";
|
||||
import { CalendarIssueBlocks, ICalendarDate, CalendarInlineCreateIssueForm } from "components/issues";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
@@ -17,10 +17,11 @@ type Props = {
|
||||
date: ICalendarDate;
|
||||
issues: IIssueGroupedStructure | null;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
};
|
||||
|
||||
export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
const { date, issues, quickActions } = props;
|
||||
const { date, issues, quickActions, enableQuickIssueCreate } = props;
|
||||
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
@@ -29,35 +30,56 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
const issuesList = issues ? (issues as IIssueGroupedStructure)[renderDateFormat(date.date)] : null;
|
||||
|
||||
return (
|
||||
<Droppable droppableId={renderDateFormat(date.date)}>
|
||||
{(provided, snapshot) => (
|
||||
<>
|
||||
<div className="group w-full h-full relative flex flex-col bg-custom-background-90">
|
||||
{/* header */}
|
||||
<div
|
||||
className={`flex-grow p-2 space-y-1 w-full flex flex-col overflow-hidden ${
|
||||
snapshot.isDraggingOver || date.date.getDay() === 0 || date.date.getDay() === 6
|
||||
className={`text-xs text-right flex-shrink-0 py-1 px-2 ${
|
||||
calendarLayout === "month" // if month layout, highlight current month days
|
||||
? date.is_current_month
|
||||
? "font-medium"
|
||||
: "text-custom-text-300"
|
||||
: "font-medium" // if week layout, highlight all days
|
||||
} ${
|
||||
date.date.getDay() === 0 || date.date.getDay() === 6
|
||||
? "bg-custom-background-90"
|
||||
: "bg-custom-background-100"
|
||||
} ${calendarLayout === "month" ? "min-h-[9rem]" : ""}`}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
}`}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={`text-xs text-right ${
|
||||
calendarLayout === "month" // if month layout, highlight current month days
|
||||
? date.is_current_month
|
||||
? "font-medium"
|
||||
: "text-custom-text-300"
|
||||
: "font-medium" // if week layout, highlight all days
|
||||
}`}
|
||||
>
|
||||
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
|
||||
{date.date.getDate()}
|
||||
</div>
|
||||
<CalendarIssueBlocks issues={issuesList} quickActions={quickActions} />
|
||||
{provided.placeholder}
|
||||
</>
|
||||
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
|
||||
{date.date.getDate()}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
{/* content */}
|
||||
<div className="w-full h-full">
|
||||
<Droppable droppableId={renderDateFormat(date.date)} isDropDisabled={false}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`h-full w-full overflow-y-auto select-none ${
|
||||
snapshot.isDraggingOver || date.date.getDay() === 0 || date.date.getDay() === 6
|
||||
? "bg-custom-background-90"
|
||||
: "bg-custom-background-100"
|
||||
} ${calendarLayout === "month" ? "min-h-[9rem]" : ""}`}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<CalendarIssueBlocks issues={issuesList} quickActions={quickActions} />
|
||||
{enableQuickIssueCreate && (
|
||||
<div className="py-1 px-2">
|
||||
<CalendarInlineCreateIssueForm
|
||||
groupId={renderDateFormat(date.date)}
|
||||
prePopulatedData={{
|
||||
target_date: renderDateFormat(date.date),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./header";
|
||||
export * from "./issue-blocks";
|
||||
export * from "./week-days";
|
||||
export * from "./week-header";
|
||||
export * from "./inline-create-issue-form";
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
// store
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
|
||||
// constants
|
||||
import { createIssuePayload } from "constants/issue";
|
||||
|
||||
// icons
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
groupId?: string;
|
||||
dependencies?: any[];
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
};
|
||||
|
||||
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>, deps: any[]) => {
|
||||
const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);
|
||||
|
||||
const router = useRouter();
|
||||
const { moduleId, cycleId, viewId } = router.query;
|
||||
|
||||
const container = document.getElementById(`calendar-view-${cycleId ?? moduleId ?? viewId}`);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const { right } = ref.current.getBoundingClientRect();
|
||||
|
||||
const width = right;
|
||||
|
||||
const innerWidth = container?.getBoundingClientRect().width ?? window.innerWidth;
|
||||
|
||||
if (width > innerWidth) setIsThereSpaceOnRight(false);
|
||||
else setIsThereSpaceOnRight(true);
|
||||
}, [ref, deps, container]);
|
||||
|
||||
return isThereSpaceOnRight;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const Inputs = (props: any) => {
|
||||
const { register, setFocus, projectDetails } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="text-sm font-medium leading-5 text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full pr-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { prePopulatedData, dependencies = [], groupId } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
// store
|
||||
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore();
|
||||
|
||||
// ref
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
register,
|
||||
setFocus,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IIssue>({ defaultValues });
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
// derived values
|
||||
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
Object.keys(errors).forEach((key) => {
|
||||
const error = errors[key as keyof IIssue];
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}, [errors, setToastAlert]);
|
||||
|
||||
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref, dependencies);
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
// resetting the form so that user can add another issue quickly
|
||||
reset({ ...defaultValues, ...(prePopulatedData ?? {}) });
|
||||
|
||||
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
labels_list:
|
||||
formData.labels_list?.length !== 0
|
||||
? formData.labels_list
|
||||
: prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none"
|
||||
? [prePopulatedData.labels as any]
|
||||
: [],
|
||||
assignees_list:
|
||||
formData.assignees_list?.length !== 0
|
||||
? formData.assignees_list
|
||||
: prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none"
|
||||
? [prePopulatedData.assignees as any]
|
||||
: [],
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddStore.createIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
{
|
||||
group_id: groupId ?? null,
|
||||
sub_group_id: null,
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
} catch (err: any) {
|
||||
Object.keys(err || {}).forEach((key) => {
|
||||
const error = err?.[key];
|
||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorTitle || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-in-out duration-200 transform"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-in-out duration-200 transform"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`transition-all z-20 w-full ${
|
||||
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
|
||||
}`}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="flex w-full px-1.5 border-[0.5px] border-custom-border-100 rounded z-50 items-center gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm transition-opacity"
|
||||
>
|
||||
<Inputs register={register} setFocus={setFocus} projectDetails={projectDetails} />
|
||||
</form>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
{!isOpen && (
|
||||
<div className="hidden group-hover:block border-[0.5px] border-custom-border-200 rounded">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center gap-x-[6px] text-custom-primary-100 px-1 py-1.5 rounded-md"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -17,42 +17,44 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 h-full w-full overflow-y-auto p-0.5">
|
||||
<>
|
||||
{issues?.map((issue, index) => (
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<Link href={`/${workspaceSlug?.toString()}/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a
|
||||
className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
|
||||
snapshot.isDragging
|
||||
? "shadow-custom-shadow-rg bg-custom-background-90"
|
||||
: "bg-custom-background-100 hover:bg-custom-background-90"
|
||||
}`}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<span
|
||||
className="h-full w-0.5 rounded flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</div>
|
||||
<h6 className="text-xs flex-grow truncate">{issue.name}</h6>
|
||||
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div>
|
||||
{/* <IssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
|
||||
/> */}
|
||||
</a>
|
||||
</Link>
|
||||
<div
|
||||
className="p-1 px-2 relative"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||
)}
|
||||
<Link href={`/${workspaceSlug?.toString()}/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a
|
||||
className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
|
||||
snapshot.isDragging
|
||||
? "shadow-custom-shadow-rg bg-custom-background-90"
|
||||
: "bg-custom-background-100 hover:bg-custom-background-90"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="h-full w-0.5 rounded flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</div>
|
||||
<h6 className="text-xs flex-grow truncate">{issue.name}</h6>
|
||||
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,12 +11,16 @@ import { IIssueGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
|
||||
export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore();
|
||||
const {
|
||||
cycleIssue: cycleIssueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
issueDetail: issueDetailStore,
|
||||
cycleIssueCalendarView: cycleIssueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
|
||||
// TODO: add drag and drop functionality
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
@@ -26,7 +30,7 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
|
||||
cycleIssueCalendarViewStore?.handleDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const issues = cycleIssueStore.getIssues;
|
||||
|
||||
@@ -15,12 +15,12 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
moduleIssue: moduleIssueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
issueDetail: issueDetailStore,
|
||||
moduleIssueCalendarView: moduleIssueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
|
||||
// TODO: add drag and drop functionality
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
|
||||
moduleIssueCalendarViewStore?.handleDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const issues = moduleIssueStore.getIssues;
|
||||
|
||||
@@ -11,12 +11,16 @@ import { IIssueGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
|
||||
export const CalendarLayout: React.FC = observer(() => {
|
||||
const { issue: issueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore();
|
||||
const {
|
||||
issue: issueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
issueDetail: issueDetailStore,
|
||||
issueCalendarView: issueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
// TODO: add drag and drop functionality
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
@@ -26,7 +30,7 @@ export const CalendarLayout: React.FC = observer(() => {
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
|
||||
issueCalendarViewStore?.handleDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const issues = issueStore.getIssues;
|
||||
|
||||
@@ -15,12 +15,12 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
||||
projectViewIssues: projectViewIssuesStore,
|
||||
issueFilter: issueFilterStore,
|
||||
issueDetail: issueDetailStore,
|
||||
projectViewIssueCalendarView: projectViewIssueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
// TODO: add drag and drop functionality
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
|
||||
projectViewIssueCalendarViewStore?.handleDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const issues = projectViewIssuesStore.getIssues;
|
||||
|
||||
@@ -15,10 +15,11 @@ type Props = {
|
||||
issues: IIssueGroupedStructure | null;
|
||||
week: ICalendarWeek | undefined;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
};
|
||||
|
||||
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
const { issues, week, quickActions } = props;
|
||||
const { issues, week, quickActions, enableQuickIssueCreate } = props;
|
||||
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
@@ -37,7 +38,13 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null;
|
||||
|
||||
return (
|
||||
<CalendarDayTile key={renderDateFormat(date.date)} date={date} issues={issues} quickActions={quickActions} />
|
||||
<CalendarDayTile
|
||||
key={renderDateFormat(date.date)}
|
||||
date={date}
|
||||
issues={issues}
|
||||
quickActions={quickActions}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,14 @@ import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
|
||||
import { GanttChartRoot, IBlockUpdateData, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
|
||||
// types
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
|
||||
export const CycleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
@@ -21,26 +21,23 @@ export const CycleGanttLayout: React.FC = observer(() => {
|
||||
|
||||
const issues = cycleIssueStore.getIssues;
|
||||
|
||||
const updateIssue = (block: any, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
cycleIssueStore.updateGanttIssueStructure(workspaceSlug.toString(), cycleId.toString(), block, payload);
|
||||
};
|
||||
|
||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
readOnly={!isAllowed}
|
||||
/>
|
||||
<div className="w-full h-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
blockUpdateHandler={(block, payload) => {
|
||||
// TODO: update mutation logic
|
||||
// updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
||||
}}
|
||||
blockUpdateHandler={updateIssue}
|
||||
BlockRender={IssueGanttBlock}
|
||||
SidebarBlockRender={IssueGanttSidebarBlock}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./cycle-root";
|
||||
export * from "./module-root";
|
||||
export * from "./project-view-root";
|
||||
export * from "./root";
|
||||
export * from "./inline-create-issue-form";
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
// store
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// constants
|
||||
import { createIssuePayload } from "constants/issue";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const Inputs = (props: any) => {
|
||||
const { register, setFocus } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { prePopulatedData } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
// store
|
||||
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IIssue>({ defaultValues });
|
||||
|
||||
// ref
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
// hooks
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// derived values
|
||||
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
Object.keys(errors).forEach((key) => {
|
||||
const error = errors[key as keyof IIssue];
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}, [errors, setToastAlert]);
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
// resetting the form so that user can add another issue quickly
|
||||
reset({ ...defaultValues, ...(prePopulatedData ?? {}) });
|
||||
|
||||
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
labels_list:
|
||||
formData.labels_list?.length !== 0
|
||||
? formData.labels_list
|
||||
: prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none"
|
||||
? [prePopulatedData.labels as any]
|
||||
: [],
|
||||
start_date: renderDateFormat(new Date()),
|
||||
target_date: renderDateFormat(new Date(new Date().getTime() + 24 * 60 * 60 * 1000)),
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddStore.createIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
{
|
||||
group_id: null,
|
||||
sub_group_id: null,
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
} catch (err: any) {
|
||||
Object.keys(err || {}).forEach((key) => {
|
||||
const error = err?.[key];
|
||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorTitle || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-in-out duration-200 transform"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-in-out duration-200 transform"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<form
|
||||
ref={ref}
|
||||
className="flex py-3 px-4 border-[0.5px] border-custom-border-100 mr-2.5 items-center rounded gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
>
|
||||
<div className="w-[14px] h-[14px] rounded-full border border-custom-border-1000 flex-shrink-0" />
|
||||
<h4 className="text-sm text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
|
||||
<Inputs register={register} setFocus={setFocus} />
|
||||
</form>
|
||||
</Transition>
|
||||
|
||||
{isOpen && (
|
||||
<p className="text-xs ml-3 mt-3 italic text-custom-text-200">
|
||||
Press {"'"}Enter{"'"} to add another issue
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!isOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -4,14 +4,14 @@ import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
|
||||
import { GanttChartRoot, IBlockUpdateData, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
|
||||
// types
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
|
||||
export const ModuleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
@@ -21,26 +21,23 @@ export const ModuleGanttLayout: React.FC = observer(() => {
|
||||
|
||||
const issues = moduleIssueStore.getIssues;
|
||||
|
||||
const updateIssue = (block: any, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
moduleIssueStore.updateGanttIssueStructure(workspaceSlug.toString(), moduleId.toString(), block, payload);
|
||||
};
|
||||
|
||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
readOnly={!isAllowed}
|
||||
/>
|
||||
<div className="w-full h-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
blockUpdateHandler={(block, payload) => {
|
||||
// TODO: update mutation logic
|
||||
// updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
||||
}}
|
||||
blockUpdateHandler={updateIssue}
|
||||
BlockRender={IssueGanttBlock}
|
||||
SidebarBlockRender={IssueGanttSidebarBlock}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
|
||||
@@ -6,14 +6,14 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
|
||||
import { GanttChartRoot, IBlockUpdateData, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
|
||||
// types
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
|
||||
export const ProjectViewGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, viewId } = router.query;
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
@@ -23,26 +23,23 @@ export const ProjectViewGanttLayout: React.FC = observer(() => {
|
||||
|
||||
const issues = projectViewIssuesStore.getIssues;
|
||||
|
||||
const updateIssue = (block: any, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !viewId) return;
|
||||
|
||||
projectViewIssuesStore.updateGanttIssueStructure(workspaceSlug.toString(), viewId.toString(), block, payload);
|
||||
};
|
||||
|
||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
readOnly={!isAllowed}
|
||||
/>
|
||||
<div className="w-full h-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
blockUpdateHandler={(block, payload) => {
|
||||
// TODO: update mutation logic
|
||||
// updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
||||
}}
|
||||
blockUpdateHandler={updateIssue}
|
||||
BlockRender={IssueGanttBlock}
|
||||
SidebarBlockRender={IssueGanttSidebarBlock}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
|
||||
@@ -4,14 +4,14 @@ import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
|
||||
import { GanttChartRoot, IBlockUpdateData, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
|
||||
// types
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
|
||||
export const GanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
@@ -21,26 +21,23 @@ export const GanttLayout: React.FC = observer(() => {
|
||||
|
||||
const issues = issueStore.getIssues;
|
||||
|
||||
const updateIssue = (block: any, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
issueStore.updateGanttIssueStructure(workspaceSlug.toString(), block, payload);
|
||||
};
|
||||
|
||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
readOnly={!isAllowed}
|
||||
/>
|
||||
<div className="w-full h-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
blockUpdateHandler={(block, payload) => {
|
||||
// TODO: update mutation logic
|
||||
// updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
||||
}}
|
||||
blockUpdateHandler={updateIssue}
|
||||
BlockRender={IssueGanttBlock}
|
||||
SidebarBlockRender={IssueGanttSidebarBlock}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Draggable } from "@hello-pangea/dnd";
|
||||
// components
|
||||
import { KanBanProperties } from "./properties";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types";
|
||||
|
||||
interface IssueBlockProps {
|
||||
sub_group_id: string;
|
||||
@@ -18,10 +18,27 @@ interface IssueBlockProps {
|
||||
) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: any;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
const { sub_group_id, columnId, index, issue, isDragDisabled, handleIssues, quickActions, displayProperties } = props;
|
||||
const {
|
||||
sub_group_id,
|
||||
columnId,
|
||||
index,
|
||||
issue,
|
||||
isDragDisabled,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
displayProperties,
|
||||
states,
|
||||
labels,
|
||||
members,
|
||||
estimates,
|
||||
} = props;
|
||||
|
||||
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
|
||||
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, "update");
|
||||
@@ -37,6 +54,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{issue.tempId !== undefined && (
|
||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||
)}
|
||||
<div className="absolute top-3 right-3 hidden group-hover/kanban-block:block">
|
||||
{quickActions(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
@@ -54,7 +74,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
<div className="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div>
|
||||
<div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
|
||||
<div>
|
||||
<KanBanProperties
|
||||
sub_group_id={sub_group_id}
|
||||
@@ -62,6 +82,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
issue={issue}
|
||||
handleIssues={updateIssue}
|
||||
display_properties={displayProperties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
estimates={estimates}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// components
|
||||
import { KanbanIssueBlock } from "components/issues";
|
||||
import { IIssue } from "types";
|
||||
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types";
|
||||
|
||||
interface IssueBlocksListProps {
|
||||
sub_group_id: string;
|
||||
@@ -15,10 +15,26 @@ interface IssueBlocksListProps {
|
||||
) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
display_properties: any;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => {
|
||||
const { sub_group_id, columnId, issues, isDragDisabled, handleIssues, quickActions, display_properties } = props;
|
||||
const {
|
||||
sub_group_id,
|
||||
columnId,
|
||||
issues,
|
||||
isDragDisabled,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
display_properties,
|
||||
states,
|
||||
labels,
|
||||
members,
|
||||
estimates,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -35,6 +51,10 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
|
||||
columnId={columnId}
|
||||
sub_group_id={sub_group_id}
|
||||
isDragDisabled={isDragDisabled}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
estimates={estimates}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -5,9 +5,9 @@ import { Droppable } from "@hello-pangea/dnd";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
||||
import { KanbanIssueBlocksList } from "components/issues";
|
||||
import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
|
||||
|
||||
@@ -29,6 +29,12 @@ export interface IGroupByKanBan {
|
||||
display_properties: any;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
priorities: any;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
@@ -45,6 +51,12 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
display_properties,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
states,
|
||||
labels,
|
||||
members,
|
||||
priorities,
|
||||
estimates,
|
||||
enableQuickIssueCreate,
|
||||
} = props;
|
||||
|
||||
const verticalAlignPosition = (_list: any) =>
|
||||
@@ -93,6 +105,10 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
estimates={estimates}
|
||||
/>
|
||||
) : (
|
||||
isDragDisabled && (
|
||||
@@ -106,6 +122,16 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
)}
|
||||
</Droppable>
|
||||
</div>
|
||||
{enableQuickIssueCreate && (
|
||||
<BoardInlineCreateIssueForm
|
||||
groupId={getValueFromObject(_list, listKey) as string}
|
||||
subGroupId={sub_group_id}
|
||||
prePopulatedData={{
|
||||
...(group_by && { [group_by]: getValueFromObject(_list, listKey) }),
|
||||
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -128,14 +154,14 @@ export interface IKanBan {
|
||||
display_properties: any;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
|
||||
states: any;
|
||||
states: IState[] | null;
|
||||
stateGroups: any;
|
||||
priorities: any;
|
||||
labels: any;
|
||||
members: any;
|
||||
projects: any;
|
||||
estimates: any;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
projects: IProject[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
}
|
||||
|
||||
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
@@ -156,6 +182,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
members,
|
||||
projects,
|
||||
estimates,
|
||||
enableQuickIssueCreate,
|
||||
} = props;
|
||||
|
||||
const { project: projectStore, issueKanBanView: issueKanBanViewStore } = useMobxStore();
|
||||
@@ -176,6 +203,12 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
display_properties={display_properties}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -193,6 +226,12 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
display_properties={display_properties}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -210,6 +249,12 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
display_properties={display_properties}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -227,6 +272,12 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
display_properties={display_properties}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -244,6 +295,12 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
display_properties={display_properties}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -261,6 +318,12 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
display_properties={display_properties}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./block";
|
||||
export * from "./roots";
|
||||
export * from "./blocks-list";
|
||||
export * from "./cycle-root";
|
||||
export * from "./module-root";
|
||||
export * from "./root";
|
||||
export * from "./inline-create-issue-form";
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// store
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
|
||||
// constants
|
||||
import { createIssuePayload } from "constants/issue";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
groupId?: string;
|
||||
subGroupId?: string;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const Inputs = (props: any) => {
|
||||
const { register, setFocus, projectDetails } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium leading-5 text-custom-text-300">{projectDetails?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 pl-0 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BoardInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { prePopulatedData, groupId, subGroupId } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
// store
|
||||
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore();
|
||||
|
||||
// ref
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
register,
|
||||
setFocus,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IIssue>({ defaultValues });
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
// derived values
|
||||
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
Object.keys(errors).forEach((key) => {
|
||||
const error = errors[key as keyof IIssue];
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}, [errors, setToastAlert]);
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
// resetting the form so that user can add another issue quickly
|
||||
reset({ ...defaultValues, ...(prePopulatedData ?? {}) });
|
||||
|
||||
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
labels_list:
|
||||
formData.labels_list && formData.labels_list.length !== 0
|
||||
? formData.labels_list
|
||||
: prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none"
|
||||
? [prePopulatedData.labels as any]
|
||||
: [],
|
||||
assignees_list:
|
||||
formData.assignees_list && formData.assignees_list.length !== 0
|
||||
? formData.assignees_list
|
||||
: prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none"
|
||||
? [prePopulatedData.assignees as any]
|
||||
: [],
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddStore.createIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
{
|
||||
group_id: groupId ?? null,
|
||||
sub_group_id: subGroupId ?? null,
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
} catch (err: any) {
|
||||
Object.keys(err || {}).forEach((key) => {
|
||||
const error = err?.[key];
|
||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorTitle || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-in-out duration-200 transform"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-in-out duration-200 transform"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="flex flex-col border-[0.5px] border-custom-border-100 justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
>
|
||||
<Inputs register={register} setFocus={setFocus} projectDetails={projectDetails} />
|
||||
</form>
|
||||
</Transition>
|
||||
|
||||
{isOpen && (
|
||||
<p className="text-xs ml-3 italic text-custom-text-200">
|
||||
Press {"'"}Enter{"'"} to add another issue
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!isOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -10,190 +10,196 @@ import { IssuePropertyAssignee } from "../properties/assignee";
|
||||
import { IssuePropertyEstimates } from "../properties/estimates";
|
||||
import { IssuePropertyDate } from "../properties/date";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite, TIssuePriorities } from "types";
|
||||
|
||||
export interface IKanBanProperties {
|
||||
sub_group_id: string;
|
||||
columnId: string;
|
||||
issue: any;
|
||||
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
|
||||
issue: IIssue;
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void;
|
||||
display_properties: any;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const KanBanProperties: React.FC<IKanBanProperties> = observer(
|
||||
({ sub_group_id, columnId: group_id, issue, handleIssues, display_properties }) => {
|
||||
const handleState = (id: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, state: id }
|
||||
);
|
||||
};
|
||||
export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) => {
|
||||
const {
|
||||
sub_group_id,
|
||||
columnId: group_id,
|
||||
issue,
|
||||
handleIssues,
|
||||
display_properties,
|
||||
states,
|
||||
labels,
|
||||
members,
|
||||
estimates,
|
||||
} = props;
|
||||
|
||||
const handlePriority = (id: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, priority: id }
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabel = (ids: string[]) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, labels: ids }
|
||||
);
|
||||
};
|
||||
|
||||
const handleAssignee = (ids: string[]) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, assignees: ids }
|
||||
);
|
||||
};
|
||||
|
||||
const handleStartDate = (date: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, start_date: date }
|
||||
);
|
||||
};
|
||||
|
||||
const handleTargetDate = (date: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, target_date: date }
|
||||
);
|
||||
};
|
||||
|
||||
const handleEstimate = (id: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, estimate_point: id }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex gap-2 overflow-x-auto whitespace-nowrap">
|
||||
{/* basic properties */}
|
||||
{/* state */}
|
||||
{display_properties && display_properties?.state && (
|
||||
<IssuePropertyState
|
||||
value={issue?.state || null}
|
||||
dropdownArrow={false}
|
||||
onChange={(id: string) => handleState(id)}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* priority */}
|
||||
{display_properties && display_properties?.priority && (
|
||||
<IssuePropertyPriority
|
||||
value={issue?.priority || null}
|
||||
dropdownArrow={false}
|
||||
onChange={(id: string) => handlePriority(id)}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* label */}
|
||||
{display_properties && display_properties?.labels && (
|
||||
<IssuePropertyLabels
|
||||
value={issue?.labels || null}
|
||||
dropdownArrow={false}
|
||||
onChange={(ids: string[]) => handleLabel(ids)}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* assignee */}
|
||||
{display_properties && display_properties?.assignee && (
|
||||
<IssuePropertyAssignee
|
||||
value={issue?.assignees || null}
|
||||
dropdownArrow={false}
|
||||
onChange={(ids: string[]) => handleAssignee(ids)}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* start date */}
|
||||
{display_properties && display_properties?.start_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* target/due date */}
|
||||
{display_properties && display_properties?.due_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* estimates */}
|
||||
{display_properties && display_properties?.estimate && (
|
||||
<IssuePropertyEstimates
|
||||
value={issue?.estimate_point?.toString() || null}
|
||||
dropdownArrow={false}
|
||||
onChange={(id: string) => handleEstimate(id)}
|
||||
disabled={false}
|
||||
workspaceSlug={issue?.workspace_detail?.slug || null}
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* extra render properties */}
|
||||
{/* sub-issues */}
|
||||
{display_properties && display_properties?.sub_issue_count && (
|
||||
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
|
||||
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center cursor-pointer">
|
||||
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
|
||||
<Layers width={10} strokeWidth={2} />
|
||||
</div>
|
||||
<div className="pl-0.5 pr-1 text-xs">{issue.sub_issues_count}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* attachments */}
|
||||
{display_properties && display_properties?.attachment_count && (
|
||||
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
||||
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center cursor-pointer">
|
||||
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
|
||||
<Paperclip width={10} strokeWidth={2} />
|
||||
</div>
|
||||
<div className="pl-0.5 pr-1 text-xs">{issue.attachment_count}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* link */}
|
||||
{display_properties && display_properties?.link && (
|
||||
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center cursor-pointer">
|
||||
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
|
||||
<Link width={10} strokeWidth={2} />
|
||||
</div>
|
||||
<div className="pl-0.5 pr-1 text-xs">{issue.link_count}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
const handleState = (state: IState) => {
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, state: state.id }
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handlePriority = (value: TIssuePriorities) => {
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, priority: value }
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabel = (ids: string[]) => {
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, labels_list: ids }
|
||||
);
|
||||
};
|
||||
|
||||
const handleAssignee = (ids: string[]) => {
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, assignees_list: ids }
|
||||
);
|
||||
};
|
||||
|
||||
const handleStartDate = (date: string) => {
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, start_date: date }
|
||||
);
|
||||
};
|
||||
|
||||
const handleTargetDate = (date: string) => {
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, target_date: date }
|
||||
);
|
||||
};
|
||||
|
||||
const handleEstimate = (value: number | null) => {
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, estimate_point: value }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap whitespace-nowrap">
|
||||
{/* basic properties */}
|
||||
{/* state */}
|
||||
{display_properties && display_properties?.state && (
|
||||
<IssuePropertyState
|
||||
value={issue?.state_detail || null}
|
||||
onChange={handleState}
|
||||
states={states}
|
||||
disabled={false}
|
||||
hideDropdownArrow={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* priority */}
|
||||
{display_properties && display_properties?.priority && (
|
||||
<IssuePropertyPriority
|
||||
value={issue?.priority || null}
|
||||
onChange={handlePriority}
|
||||
disabled={false}
|
||||
hideDropdownArrow={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* label */}
|
||||
{display_properties && display_properties?.labels && (
|
||||
<IssuePropertyLabels
|
||||
value={issue?.labels || null}
|
||||
onChange={handleLabel}
|
||||
labels={labels}
|
||||
disabled={false}
|
||||
hideDropdownArrow={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* assignee */}
|
||||
{display_properties && display_properties?.assignee && (
|
||||
<IssuePropertyAssignee
|
||||
value={issue?.assignees || null}
|
||||
hideDropdownArrow={true}
|
||||
onChange={handleAssignee}
|
||||
members={members}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* start date */}
|
||||
{display_properties && display_properties?.start_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
disabled={false}
|
||||
placeHolder="Start date"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* target/due date */}
|
||||
{display_properties && display_properties?.due_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
disabled={false}
|
||||
placeHolder="Target date"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* estimates */}
|
||||
{display_properties && display_properties?.estimate && (
|
||||
<IssuePropertyEstimates
|
||||
value={issue?.estimate_point || null}
|
||||
onChange={handleEstimate}
|
||||
estimatePoints={estimates}
|
||||
disabled={false}
|
||||
hideDropdownArrow={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* extra render properties */}
|
||||
{/* sub-issues */}
|
||||
{display_properties && display_properties?.sub_issue_count && (
|
||||
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}>
|
||||
<div className="flex-shrink-0 border-[0.5px] border-custom-border-300 overflow-hidden rounded flex justify-center items-center gap-2 px-2.5 py-1 h-5">
|
||||
<Layers className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||
<div className="text-xs">{issue.sub_issues_count}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* attachments */}
|
||||
{display_properties && display_properties?.attachment_count && (
|
||||
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
||||
<div className="flex-shrink-0 border-[0.5px] border-custom-border-300 overflow-hidden rounded flex justify-center items-center gap-2 px-2.5 py-1 h-5">
|
||||
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||
<div className="text-xs">{issue.attachment_count}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* link */}
|
||||
{display_properties && display_properties?.link && (
|
||||
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||
<div className="flex-shrink-0 border-[0.5px] border-custom-border-300 overflow-hidden rounded flex justify-center items-center gap-2 px-2.5 py-1 h-5">
|
||||
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||
<div className="text-xs">{issue.link_count}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
+19
-12
@@ -5,9 +5,11 @@ import { DragDropContext } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
import { CycleIssueQuickActions } from "components/issues";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
@@ -25,7 +27,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
|
||||
const issues = cycleIssueStore?.getIssues;
|
||||
|
||||
@@ -60,12 +62,12 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
if (action === "update") {
|
||||
cycleIssueStore.updateIssueStructure(group_by, null, issue);
|
||||
cycleIssueStore.updateIssueStructure(group_by, sub_group_by, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") cycleIssueStore.deleteIssue(group_by, null, issue);
|
||||
if (action === "delete") cycleIssueStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
if (action === "remove" && issue.bridge_id) {
|
||||
cycleIssueStore.deleteIssue(group_by, null, issue);
|
||||
cycleIssueStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
cycleIssueStore.removeIssueFromCycle(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
@@ -81,13 +83,18 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||
};
|
||||
|
||||
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null;
|
||||
|
||||
const states = projectStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const labels = projectStore?.projectLabels || null;
|
||||
const members = projectStore?.projectMembers || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = projectStore?.projectStates || null;
|
||||
const estimates = null;
|
||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||
const estimates =
|
||||
projectDetails?.estimate !== null
|
||||
? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
||||
@@ -113,9 +120,9 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
@@ -138,9 +145,9 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./cycle-root";
|
||||
export * from "./module-root";
|
||||
export * from "./profile-issues-root";
|
||||
export * from "./project-root";
|
||||
export * from "./project-view-root";
|
||||
+16
-9
@@ -5,9 +5,11 @@ import { DragDropContext } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
import { ModuleIssueQuickActions } from "components/issues";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
@@ -25,7 +27,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
|
||||
const issues = moduleIssueStore?.getIssues;
|
||||
|
||||
@@ -81,13 +83,18 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||
};
|
||||
|
||||
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null;
|
||||
|
||||
const states = projectStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const labels = projectStore?.projectLabels || null;
|
||||
const members = projectStore?.projectMembers || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = projectStore?.projectStates || null;
|
||||
const estimates = null;
|
||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||
const estimates =
|
||||
projectDetails?.estimate !== null
|
||||
? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
||||
@@ -113,9 +120,9 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
@@ -138,9 +145,9 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
+6
-7
@@ -5,8 +5,8 @@ import { DragDropContext } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
import { ProjectIssueQuickActions } from "components/issues";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
@@ -79,7 +79,6 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
const members = projectStore?.projectMembers || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = projectStore?.workspaceProjects || null;
|
||||
const estimates = null;
|
||||
|
||||
return (
|
||||
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
||||
@@ -104,9 +103,9 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
estimates={null}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
@@ -128,9 +127,9 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
estimates={null}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
+19
-11
@@ -1,13 +1,15 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
import { ProjectIssueQuickActions } from "components/issues";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
@@ -15,9 +17,9 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
|
||||
export interface IKanBanLayout {}
|
||||
|
||||
export const KanBanLayout: FC = observer(() => {
|
||||
export const KanBanLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
project: projectStore,
|
||||
@@ -72,13 +74,18 @@ export const KanBanLayout: FC = observer(() => {
|
||||
issueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||
};
|
||||
|
||||
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null;
|
||||
|
||||
const states = projectStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const labels = projectStore?.projectLabels || null;
|
||||
const members = projectStore?.projectMembers || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = projectStore?.projectStates || null;
|
||||
const estimates = null;
|
||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||
const estimates =
|
||||
projectDetails?.estimate !== null
|
||||
? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
||||
@@ -103,9 +110,10 @@ export const KanBanLayout: FC = observer(() => {
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
@@ -127,9 +135,9 @@ export const KanBanLayout: FC = observer(() => {
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
+3
-3
@@ -4,8 +4,8 @@ import { DragDropContext } from "@hello-pangea/dnd";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
@@ -14,7 +14,7 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
|
||||
export interface IViewKanBanLayout {}
|
||||
|
||||
export const ViewKanBanLayout: React.FC = observer(() => {
|
||||
export const ProjectViewKanBanLayout: React.FC = observer(() => {
|
||||
const {
|
||||
project: projectStore,
|
||||
issue: issueStore,
|
||||
@@ -7,7 +7,7 @@ import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
||||
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
|
||||
import { KanBan } from "./default";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
|
||||
|
||||
@@ -19,6 +19,11 @@ interface ISubGroupSwimlaneHeader {
|
||||
listKey: string;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
projects: IProject[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
issues,
|
||||
@@ -71,13 +76,13 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||
display_properties: any;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
states: any;
|
||||
states: IState[] | null;
|
||||
stateGroups: any;
|
||||
priorities: any;
|
||||
labels: any;
|
||||
members: any;
|
||||
projects: any;
|
||||
estimates: any;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
projects: IProject[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
const {
|
||||
@@ -148,6 +153,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
members={members}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -171,13 +177,13 @@ export interface IKanBanSwimLanes {
|
||||
display_properties: any;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
states: any;
|
||||
states: IState[] | null;
|
||||
stateGroups: any;
|
||||
priorities: any;
|
||||
labels: any;
|
||||
members: any;
|
||||
projects: any;
|
||||
estimates: any;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
projects: IProject[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
@@ -213,6 +219,11 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`id`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -225,6 +236,11 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`key`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -237,6 +253,11 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`key`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -249,6 +270,11 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`id`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -261,6 +287,11 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`member.id`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -273,6 +304,11 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`member.id`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user