diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index e39f91f6f..6343c740e 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -49,7 +49,7 @@ USER root RUN apk --update --no-cache add "bash~=5.1" COPY ./bin ./bin/ -RUN chmod +x ./bin/channel-worker ./bin/takeoff ./bin/worker +RUN chmod +x ./bin/takeoff ./bin/worker USER captain diff --git a/apiserver/Procfile b/apiserver/Procfile index 026a3f953..35f6e9aa8 100644 --- a/apiserver/Procfile +++ b/apiserver/Procfile @@ -1,3 +1,2 @@ web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile - -worker: python manage.py rqworker -channel-worker: python manage.py runworker issue-activites \ No newline at end of file +worker: python manage.py rqworker \ No newline at end of file diff --git a/apiserver/bin/channel-worker b/apiserver/bin/channel-worker deleted file mode 100755 index 90ba63d50..000000000 --- a/apiserver/bin/channel-worker +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -e - -python manage.py wait_for_db -python manage.py migrate -python manage.py runworker issue-activites \ No newline at end of file diff --git a/apiserver/plane/api/consumers/__init__.py b/apiserver/plane/api/consumers/__init__.py deleted file mode 100644 index cbf41cdfa..000000000 --- a/apiserver/plane/api/consumers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .issue_consumer import IssueConsumer \ No newline at end of file diff --git a/apiserver/plane/api/consumers/issue_consumer.py b/apiserver/plane/api/consumers/issue_consumer.py deleted file mode 100644 index 38eb69967..000000000 --- a/apiserver/plane/api/consumers/issue_consumer.py +++ /dev/null @@ -1,547 +0,0 @@ -from channels.generic.websocket import SyncConsumer -import json -from plane.db.models import IssueActivity, Project, User, Issue, State, Label - - -class IssueConsumer(SyncConsumer): - - # Track Chnages in name - def track_name( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - if current_instance.get("name") != requested_data.get("name"): - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("name"), - new_value=requested_data.get("name"), - field="name", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the start date to {requested_data.get('name')}", - ) - ) - - # Track changes in parent issue - def track_parent( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - if current_instance.get("parent") != requested_data.get("parent"): - - if requested_data.get("parent") == None: - old_parent = Issue.objects.get(pk=current_instance.get("parent")) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=f"{project.identifier}-{old_parent.sequence_id}", - new_value=None, - field="parent", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the parent issue to None", - old_identifier=old_parent.id, - new_identifier=None, - ) - ) - else: - new_parent = Issue.objects.get(pk=requested_data.get("parent")) - old_parent = Issue.objects.filter( - pk=current_instance.get("parent") - ).first() - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=f"{project.identifier}-{old_parent.sequence_id}" - if old_parent is not None - else None, - new_value=f"{project.identifier}-{new_parent.sequence_id}", - field="parent", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the parent issue to {new_parent.name}", - old_identifier=old_parent.id - if old_parent is not None - else None, - new_identifier=new_parent.id, - ) - ) - - # Track changes in priority - def track_priority( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - if current_instance.get("priority") != requested_data.get("priority"): - if requested_data.get("priority") == None: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("parent"), - new_value=requested_data.get("parent"), - field="priority", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the priority to None", - ) - ) - else: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("priority"), - new_value=requested_data.get("priority"), - field="priority", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the priority to {requested_data.get('priority')}", - ) - ) - - # Track chnages in state of the issue - def track_state( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - if current_instance.get("state") != requested_data.get("state"): - - new_state = State.objects.get(pk=requested_data.get("state", None)) - old_state = State.objects.get(pk=current_instance.get("state", None)) - - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=old_state.name, - new_value=new_state.name, - field="state", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the state to {new_state.name}", - old_identifier=old_state.id, - new_identifier=new_state.id, - ) - ) - - # Track issue description - def track_description( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - if current_instance.get("description_html") != requested_data.get("description_html"): - - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("description_html"), - new_value=requested_data.get("description_html"), - field="description", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the description to {requested_data.get('description_html')}", - ) - ) - - # Track changes in issue target date - def track_target_date( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - if current_instance.get("target_date") != requested_data.get("target_date"): - if requested_data.get("target_date") == None: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("target_date"), - new_value=requested_data.get("target_date"), - field="target_date", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the target date to None", - ) - ) - else: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("target_date"), - new_value=requested_data.get("target_date"), - field="target_date", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the target date to {requested_data.get('target_date')}", - ) - ) - - # Track changes in issue start date - def track_start_date( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - if current_instance.get("start_date") != requested_data.get("start_date"): - if requested_data.get("start_date") == None: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("start_date"), - new_value=requested_data.get("start_date"), - field="start_date", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the start date to None", - ) - ) - else: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("start_date"), - new_value=requested_data.get("start_date"), - field="start_date", - project=project, - workspace=project.workspace, - comment=f"{actor.email} updated the start date to {requested_data.get('start_date')}", - ) - ) - - # Track changes in issue labels - def track_labels( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - # Label Addition - if len(requested_data.get("labels_list")) > len(current_instance.get("labels")): - - for label in requested_data.get("labels_list"): - if label not in current_instance.get("labels"): - label = Label.objects.get(pk=label) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value="", - new_value=label.name, - field="labels", - project=project, - workspace=project.workspace, - comment=f"{actor.email} added label {label.name}", - new_identifier=label.id, - old_identifier=None, - ) - ) - - # Label Removal - if len(requested_data.get("labels_list")) < len(current_instance.get("labels")): - - for label in current_instance.get("labels"): - if label not in requested_data.get("labels_list"): - label = Label.objects.get(pk=label) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=label.name, - new_value="", - field="labels", - project=project, - workspace=project.workspace, - comment=f"{actor.email} removed label {label.name}", - old_identifier=label.id, - new_identifier=None, - ) - ) - - # Track changes in issue assignees - def track_assignees( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - - # Assignee Addition - if len(requested_data.get("assignees_list")) > len( - current_instance.get("assignees") - ): - - for assignee in requested_data.get("assignees_list"): - if assignee not in current_instance.get("assignees"): - assignee = User.objects.get(pk=assignee) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value="", - new_value=assignee.email, - field="assignees", - project=project, - workspace=project.workspace, - comment=f"{actor.email} added assignee {assignee.email}", - new_identifier=actor.id, - ) - ) - - # Assignee Removal - if len(requested_data.get("assignees_list")) < len( - current_instance.get("assignees") - ): - - for assignee in current_instance.get("assignees"): - if assignee not in requested_data.get("assignees_list"): - assignee = User.objects.get(pk=assignee) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=assignee.email, - new_value="", - field="assignee", - project=project, - workspace=project.workspace, - comment=f"{actor.email} removed assignee {assignee.email}", - old_identifier=actor.id, - ) - ) - - # Track changes in blocking issues - def track_blocks( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - if len(requested_data.get("blocks_list")) > len( - current_instance.get("blocked_issues") - ): - - for block in requested_data.get("blocks_list"): - if ( - len( - [ - blocked - for blocked in current_instance.get("blocked_issues") - if blocked.get("block") == block - ] - ) - == 0 - ): - issue = Issue.objects.get(pk=block) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value="", - new_value=f"{project.identifier}-{issue.sequence_id}", - field="blocks", - project=project, - workspace=project.workspace, - comment=f"{actor.email} added blocking issue {project.identifier}-{issue.sequence_id}", - new_identifier=issue.id, - ) - ) - - # Blocked Issue Removal - if len(requested_data.get("blocks_list")) < len( - current_instance.get("blocked_issues") - ): - - for blocked in current_instance.get("blocked_issues"): - if blocked.get("block") not in requested_data.get("blocks_list"): - issue = Issue.objects.get(pk=blocked.get("block")) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=f"{project.identifier}-{issue.sequence_id}", - new_value="", - field="blocks", - project=project, - workspace=project.workspace, - comment=f"{actor.email} removed blocking issue {project.identifier}-{issue.sequence_id}", - old_identifier=issue.id, - ) - ) - - # Track changes in blocked_by issues - def track_blockings( - self, - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ): - if len(requested_data.get("blockers_list")) > len( - current_instance.get("blocker_issues") - ): - - for block in requested_data.get("blockers_list"): - if ( - len( - [ - blocked - for blocked in current_instance.get("blocker_issues") - if blocked.get("blocked_by") == block - ] - ) - == 0 - ): - issue = Issue.objects.get(pk=block) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value="", - new_value=f"{project.identifier}-{issue.sequence_id}", - field="blocking", - project=project, - workspace=project.workspace, - comment=f"{actor.email} added blocked by issue {project.identifier}-{issue.sequence_id}", - new_identifier=issue.id, - ) - ) - - # Blocked Issue Removal - if len(requested_data.get("blockers_list")) < len( - current_instance.get("blocker_issues") - ): - - for blocked in current_instance.get("blocker_issues"): - if blocked.get("blocked_by") not in requested_data.get("blockers_list"): - issue = Issue.objects.get(pk=blocked.get("blocked_by")) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=f"{project.identifier}-{issue.sequence_id}", - new_value="", - field="blocking", - project=project, - workspace=project.workspace, - comment=f"{actor.email} removed blocked by issue {project.identifier}-{issue.sequence_id}", - old_identifier=issue.id, - ) - ) - - # Receive message from room group - def issue_activity(self, event): - - issue_activities = [] - # Remove event type: - event.pop("type") - - requested_data = json.loads(event.get("requested_data")) - current_instance = json.loads(event.get("current_instance")) - issue_id = event.get("issue_id") - actor_id = event.get("actor_id") - project_id = event.get("project_id") - - actor = User.objects.get(pk=actor_id) - - project = Project.objects.get(pk=project_id) - - ISSUE_ACTIVITY_MAPPER = { - "name": self.track_name, - "parent": self.track_parent, - "priority": self.track_priority, - "state": self.track_state, - "description": self.track_description, - "target_date": self.track_target_date, - "start_date": self.track_start_date, - "labels_list": self.track_labels, - "assignees_list": self.track_assignees, - "blocks_list": self.track_blocks, - "blockers_list": self.track_blockings, - } - - for key in requested_data: - func = ISSUE_ACTIVITY_MAPPER.get(key, None) - if func is not None: - func( - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ) - - # Save all the values to database - IssueActivity.objects.bulk_create(issue_activities) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index ba494ec9e..8d43d90ff 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -38,4 +38,6 @@ from .issue import ( IssueStateSerializer, ) -from .module import ModuleWriteSerializer, ModuleSerializer, ModuleIssueSerializer \ No newline at end of file +from .module import ModuleWriteSerializer, ModuleSerializer, ModuleIssueSerializer + +from .api_token import APITokenSerializer \ No newline at end of file diff --git a/apiserver/plane/api/serializers/api_token.py b/apiserver/plane/api/serializers/api_token.py new file mode 100644 index 000000000..247b3f0e7 --- /dev/null +++ b/apiserver/plane/api/serializers/api_token.py @@ -0,0 +1,8 @@ +from .base import BaseSerializer +from plane.db.models import APIToken + + +class APITokenSerializer(BaseSerializer): + class Meta: + model = APIToken + fields = "__all__" diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py index 9f165dd28..2aa5ec208 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -93,7 +93,7 @@ class ModuleWriteSerializer(BaseSerializer): links = validated_data.pop("links_list", None) if members is not None: - ModuleIssue.objects.filter(module=instance).delete() + ModuleMember.objects.filter(module=instance).delete() ModuleMember.objects.bulk_create( [ ModuleMember( diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 38d2b4013..98c2e87d2 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -84,6 +84,9 @@ from plane.api.views import ( ModuleViewSet, ModuleIssueViewSet, ## End Modules + # Api Tokens + ApiTokenEndpoint, + ## End Api Tokens ) @@ -679,4 +682,8 @@ urlpatterns = [ name="project-module-issues", ), ## End Modules + # API Tokens + path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-token"), + path("api-tokens//", ApiTokenEndpoint.as_view(), name="api-token"), + ## End API Tokens ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 933315277..1212e0dca 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -72,3 +72,5 @@ from .authentication import ( ) from .module import ModuleViewSet, ModuleIssueViewSet + +from .api_token import ApiTokenEndpoint \ No newline at end of file diff --git a/apiserver/plane/api/views/api_token.py b/apiserver/plane/api/views/api_token.py new file mode 100644 index 000000000..4ed3d9de0 --- /dev/null +++ b/apiserver/plane/api/views/api_token.py @@ -0,0 +1,62 @@ +# Python import +from uuid import uuid4 + +# Third party +from rest_framework.response import Response +from rest_framework import status +from sentry_sdk import capture_exception + +# Module import +from .base import BaseAPIView +from plane.db.models import APIToken +from plane.api.serializers import APITokenSerializer + + +class ApiTokenEndpoint(BaseAPIView): + def post(self, request): + try: + + label = request.data.get("label", str(uuid4().hex)) + + api_token = APIToken.objects.create( + label=label, + user=request.user, + ) + + serializer = APITokenSerializer(api_token) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get(self, request): + try: + api_tokens = APIToken.objects.filter(user=request.user) + serializer = APITokenSerializer(api_tokens, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def delete(self, request, pk): + try: + api_token = APIToken.objects.get(pk=pk) + api_token.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except APIToken.DoesNotExist: + return Response( + {"error": "Token does not exists"}, status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index f716c2b11..37082e0ec 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -10,8 +10,6 @@ from django.core.serializers.json import DjangoJSONEncoder from rest_framework.response import Response from rest_framework import status from sentry_sdk import capture_exception -from channels.layers import get_channel_layer -from asgiref.sync import async_to_sync # Module imports from . import BaseViewSet, BaseAPIView @@ -42,6 +40,7 @@ from plane.db.models import ( CycleIssue, ModuleIssue, ) +from plane.bgtasks.issue_activites_task import issue_activity class IssueViewSet(BaseViewSet): @@ -72,12 +71,12 @@ class IssueViewSet(BaseViewSet): def perform_update(self, serializer): requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - current_instance = Issue.objects.filter(pk=self.kwargs.get("pk", None)).first() + current_instance = ( + self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() + ) if current_instance is not None: - channel_layer = get_channel_layer() - async_to_sync(channel_layer.send)( - "issue-activites", + issue_activity.delay( { "type": "issue.activity", "requested_data": requested_data, diff --git a/apiserver/plane/asgi.py b/apiserver/plane/asgi.py index b5beb1df4..7333baae3 100644 --- a/apiserver/plane/asgi.py +++ b/apiserver/plane/asgi.py @@ -1,6 +1,6 @@ import os -from channels.routing import ProtocolTypeRouter, ChannelNameRouter +from channels.routing import ProtocolTypeRouter from django.core.asgi import get_asgi_application django_asgi_app = get_asgi_application() @@ -10,15 +10,9 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") # Initialize Django ASGI application early to ensure the AppRegistry # is populated before importing code that may import ORM models. -from plane.api.consumers import IssueConsumer application = ProtocolTypeRouter( { "http": get_asgi_application(), - "channel": ChannelNameRouter( - { - "issue-activites": IssueConsumer.as_asgi(), - } - ), } ) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py new file mode 100644 index 000000000..f6debc921 --- /dev/null +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -0,0 +1,553 @@ +# Python imports +import json + +# Third Party imports +from django_rq import job +from sentry_sdk import capture_exception + +# Module imports +from plane.db.models import User, Issue, Project, Label, IssueActivity, State + + +# Track Chnages in name +def track_name( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + if current_instance.get("name") != requested_data.get("name"): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=current_instance.get("name"), + new_value=requested_data.get("name"), + field="name", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the start date to {requested_data.get('name')}", + ) + ) + + +# Track changes in parent issue +def track_parent( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + if current_instance.get("parent") != requested_data.get("parent"): + + if requested_data.get("parent") == None: + old_parent = Issue.objects.get(pk=current_instance.get("parent")) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=f"{project.identifier}-{old_parent.sequence_id}", + new_value=None, + field="parent", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the parent issue to None", + old_identifier=old_parent.id, + new_identifier=None, + ) + ) + else: + new_parent = Issue.objects.get(pk=requested_data.get("parent")) + old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=f"{project.identifier}-{old_parent.sequence_id}" + if old_parent is not None + else None, + new_value=f"{project.identifier}-{new_parent.sequence_id}", + field="parent", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the parent issue to {new_parent.name}", + old_identifier=old_parent.id if old_parent is not None else None, + new_identifier=new_parent.id, + ) + ) + + +# Track changes in priority +def track_priority( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + if current_instance.get("priority") != requested_data.get("priority"): + if requested_data.get("priority") == None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=current_instance.get("priority"), + new_value=None, + field="priority", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the priority to None", + ) + ) + else: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=current_instance.get("priority"), + new_value=requested_data.get("priority"), + field="priority", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the priority to {requested_data.get('priority')}", + ) + ) + + +# Track chnages in state of the issue +def track_state( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + if current_instance.get("state") != requested_data.get("state"): + + new_state = State.objects.get(pk=requested_data.get("state", None)) + old_state = State.objects.get(pk=current_instance.get("state", None)) + + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=old_state.name, + new_value=new_state.name, + field="state", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the state to {new_state.name}", + old_identifier=old_state.id, + new_identifier=new_state.id, + ) + ) + + +# Track issue description +def track_description( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + if current_instance.get("description_html") != requested_data.get( + "description_html" + ): + + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=current_instance.get("description_html"), + new_value=requested_data.get("description_html"), + field="description", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the description to {requested_data.get('description_html')}", + ) + ) + + +# Track changes in issue target date +def track_target_date( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + if current_instance.get("target_date") != requested_data.get("target_date"): + if requested_data.get("target_date") == None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=current_instance.get("target_date"), + new_value=requested_data.get("target_date"), + field="target_date", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the target date to None", + ) + ) + else: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=current_instance.get("target_date"), + new_value=requested_data.get("target_date"), + field="target_date", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the target date to {requested_data.get('target_date')}", + ) + ) + + +# Track changes in issue start date +def track_start_date( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + if current_instance.get("start_date") != requested_data.get("start_date"): + if requested_data.get("start_date") == None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=current_instance.get("start_date"), + new_value=requested_data.get("start_date"), + field="start_date", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the start date to None", + ) + ) + else: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=current_instance.get("start_date"), + new_value=requested_data.get("start_date"), + field="start_date", + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated the start date to {requested_data.get('start_date')}", + ) + ) + + +# Track changes in issue labels +def track_labels( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + # Label Addition + if len(requested_data.get("labels_list")) > len(current_instance.get("labels")): + + for label in requested_data.get("labels_list"): + if label not in current_instance.get("labels"): + label = Label.objects.get(pk=label) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value="", + new_value=label.name, + field="labels", + project=project, + workspace=project.workspace, + comment=f"{actor.email} added label {label.name}", + new_identifier=label.id, + old_identifier=None, + ) + ) + + # Label Removal + if len(requested_data.get("labels_list")) < len(current_instance.get("labels")): + + for label in current_instance.get("labels"): + if label not in requested_data.get("labels_list"): + label = Label.objects.get(pk=label) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=label.name, + new_value="", + field="labels", + project=project, + workspace=project.workspace, + comment=f"{actor.email} removed label {label.name}", + old_identifier=label.id, + new_identifier=None, + ) + ) + + +# Track changes in issue assignees +def track_assignees( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + + # Assignee Addition + if len(requested_data.get("assignees_list")) > len( + current_instance.get("assignees") + ): + + for assignee in requested_data.get("assignees_list"): + if assignee not in current_instance.get("assignees"): + assignee = User.objects.get(pk=assignee) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value="", + new_value=assignee.email, + field="assignees", + project=project, + workspace=project.workspace, + comment=f"{actor.email} added assignee {assignee.email}", + new_identifier=actor.id, + ) + ) + + # Assignee Removal + if len(requested_data.get("assignees_list")) < len( + current_instance.get("assignees") + ): + + for assignee in current_instance.get("assignees"): + if assignee not in requested_data.get("assignees_list"): + assignee = User.objects.get(pk=assignee) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=assignee.email, + new_value="", + field="assignee", + project=project, + workspace=project.workspace, + comment=f"{actor.email} removed assignee {assignee.email}", + old_identifier=actor.id, + ) + ) + + +# Track changes in blocking issues +def track_blocks( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + if len(requested_data.get("blocks_list")) > len( + current_instance.get("blocked_issues") + ): + + for block in requested_data.get("blocks_list"): + if ( + len( + [ + blocked + for blocked in current_instance.get("blocked_issues") + if blocked.get("block") == block + ] + ) + == 0 + ): + issue = Issue.objects.get(pk=block) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value="", + new_value=f"{project.identifier}-{issue.sequence_id}", + field="blocks", + project=project, + workspace=project.workspace, + comment=f"{actor.email} added blocking issue {project.identifier}-{issue.sequence_id}", + new_identifier=issue.id, + ) + ) + + # Blocked Issue Removal + if len(requested_data.get("blocks_list")) < len( + current_instance.get("blocked_issues") + ): + + for blocked in current_instance.get("blocked_issues"): + if blocked.get("block") not in requested_data.get("blocks_list"): + issue = Issue.objects.get(pk=blocked.get("block")) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=f"{project.identifier}-{issue.sequence_id}", + new_value="", + field="blocks", + project=project, + workspace=project.workspace, + comment=f"{actor.email} removed blocking issue {project.identifier}-{issue.sequence_id}", + old_identifier=issue.id, + ) + ) + + +# Track changes in blocked_by issues +def track_blockings( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, +): + if len(requested_data.get("blockers_list")) > len( + current_instance.get("blocker_issues") + ): + + for block in requested_data.get("blockers_list"): + if ( + len( + [ + blocked + for blocked in current_instance.get("blocker_issues") + if blocked.get("blocked_by") == block + ] + ) + == 0 + ): + issue = Issue.objects.get(pk=block) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value="", + new_value=f"{project.identifier}-{issue.sequence_id}", + field="blocking", + project=project, + workspace=project.workspace, + comment=f"{actor.email} added blocked by issue {project.identifier}-{issue.sequence_id}", + new_identifier=issue.id, + ) + ) + + # Blocked Issue Removal + if len(requested_data.get("blockers_list")) < len( + current_instance.get("blocker_issues") + ): + + for blocked in current_instance.get("blocker_issues"): + if blocked.get("blocked_by") not in requested_data.get("blockers_list"): + issue = Issue.objects.get(pk=blocked.get("blocked_by")) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=f"{project.identifier}-{issue.sequence_id}", + new_value="", + field="blocking", + project=project, + workspace=project.workspace, + comment=f"{actor.email} removed blocked by issue {project.identifier}-{issue.sequence_id}", + old_identifier=issue.id, + ) + ) + + +# Receive message from room group +@job("default") +def issue_activity(event): + try: + issue_activities = [] + + requested_data = json.loads(event.get("requested_data")) + current_instance = json.loads(event.get("current_instance")) + issue_id = event.get("issue_id") + actor_id = event.get("actor_id") + project_id = event.get("project_id") + + actor = User.objects.get(pk=actor_id) + + project = Project.objects.get(pk=project_id) + + ISSUE_ACTIVITY_MAPPER = { + "name": track_name, + "parent": track_parent, + "priority": track_priority, + "state": track_state, + "description": track_description, + "target_date": track_target_date, + "start_date": track_start_date, + "labels_list": track_labels, + "assignees_list": track_assignees, + "blocks_list": track_blocks, + "blockers_list": track_blockings, + } + + for key in requested_data: + func = ISSUE_ACTIVITY_MAPPER.get(key, None) + if func is not None: + func( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, + ) + + # Save all the values to database + _ = IssueActivity.objects.bulk_create(issue_activities) + + return + except Exception as e: + capture_exception(e) + return diff --git a/apiserver/plane/db/apps.py b/apiserver/plane/db/apps.py index c2741bba3..7d4919d08 100644 --- a/apiserver/plane/db/apps.py +++ b/apiserver/plane/db/apps.py @@ -1,52 +1,5 @@ from django.apps import AppConfig -from fieldsignals import post_save_changed class DbConfig(AppConfig): name = "plane.db" - - # def ready(self): - - # post_save_changed.connect( - # self.model_activity, - # sender=self.get_model("Issue"), - # ) - - # def model_activity(self, sender, instance, changed_fields, **kwargs): - - # verb = "created" if instance._state.adding else "changed" - - # import inspect - - # for frame_record in inspect.stack(): - # if frame_record[3] == "get_response": - # request = frame_record[0].f_locals["request"] - # REQUEST_METHOD = request.method - - # if REQUEST_METHOD == "POST": - - # self.get_model("IssueActivity").objects.create( - # issue=instance, project=instance.project, actor=instance.created_by - # ) - - # elif REQUEST_METHOD == "PATCH": - - # try: - # del changed_fields["updated_at"] - # del changed_fields["updated_by"] - # except KeyError as e: - # pass - - # for field_name, (old, new) in changed_fields.items(): - # field = field_name - # old_value = old - # new_value = new - # self.get_model("IssueActivity").objects.create( - # issue=instance, - # verb=verb, - # field=field, - # old_value=old_value, - # new_value=new_value, - # project=instance.project, - # actor=instance.updated_by, - # ) diff --git a/apiserver/plane/db/migrations/0018_auto_20230130_0119.py b/apiserver/plane/db/migrations/0018_auto_20230130_0119.py new file mode 100644 index 000000000..500bc3b28 --- /dev/null +++ b/apiserver/plane/db/migrations/0018_auto_20230130_0119.py @@ -0,0 +1,57 @@ +# Generated by Django 3.2.16 on 2023-01-29 19:49 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import plane.db.models.api_token +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0017_alter_workspace_unique_together'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_bot', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='issue', + name='description', + field=models.JSONField(blank=True, null=True), + ), + migrations.AlterField( + model_name='issue', + name='description_html', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='issue', + name='description_stripped', + field=models.TextField(blank=True, null=True), + ), + migrations.CreateModel( + name='APIToken', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('token', models.CharField(default=plane.db.models.api_token.generate_token, max_length=255, unique=True)), + ('label', models.CharField(default=plane.db.models.api_token.generate_label_token, max_length=255)), + ('user_type', models.PositiveSmallIntegerField(choices=[(0, 'Human'), (1, 'Bot')], default=0)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='apitoken_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='apitoken_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bot_tokens', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'API Token', + 'verbose_name_plural': 'API Tokems', + 'db_table': 'api_tokens', + 'ordering': ('-created_at',), + }, + ), + ] diff --git a/apiserver/plane/db/migrations/0019_auto_20230131_0049.py b/apiserver/plane/db/migrations/0019_auto_20230131_0049.py new file mode 100644 index 000000000..38412aa9e --- /dev/null +++ b/apiserver/plane/db/migrations/0019_auto_20230131_0049.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.16 on 2023-01-30 19:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0018_auto_20230130_0119'), + ] + + operations = [ + migrations.AlterField( + model_name='issueactivity', + name='new_value', + field=models.TextField(blank=True, null=True, verbose_name='New Value'), + ), + migrations.AlterField( + model_name='issueactivity', + name='old_value', + field=models.TextField(blank=True, null=True, verbose_name='Old Value'), + ), + ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index b6a19b67a..ef7ad5b8d 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -38,3 +38,5 @@ from .shortcut import Shortcut from .view import View from .module import Module, ModuleMember, ModuleIssue, ModuleLink + +from .api_token import APIToken \ No newline at end of file diff --git a/apiserver/plane/db/models/api_token.py b/apiserver/plane/db/models/api_token.py new file mode 100644 index 000000000..32ba013bc --- /dev/null +++ b/apiserver/plane/db/models/api_token.py @@ -0,0 +1,39 @@ +# Python imports +from uuid import uuid4 + +# Django imports +from django.db import models +from django.conf import settings + +from .base import BaseModel + + +def generate_label_token(): + return uuid4().hex + + +def generate_token(): + return uuid4().hex + uuid4().hex + + +class APIToken(BaseModel): + + token = models.CharField(max_length=255, unique=True, default=generate_token) + label = models.CharField(max_length=255, default=generate_label_token) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="bot_tokens", + ) + user_type = models.PositiveSmallIntegerField( + choices=((0, "Human"), (1, "Bot")), default=0 + ) + + class Meta: + verbose_name = "API Token" + verbose_name_plural = "API Tokems" + db_table = "api_tokens" + ordering = ("-created_at",) + + def __str__(self): + return str(self.user.name) diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index f69f11574..c3984b3d2 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -145,12 +145,8 @@ class IssueActivity(ProjectBaseModel): field = models.CharField( max_length=255, verbose_name="Field Name", blank=True, null=True ) - old_value = models.CharField( - max_length=255, verbose_name="Old Value", blank=True, null=True - ) - new_value = models.CharField( - max_length=255, verbose_name="New Value", blank=True, null=True - ) + old_value = models.TextField(verbose_name="Old Value", blank=True, null=True) + new_value = models.TextField(verbose_name="New Value", blank=True, null=True) comment = models.TextField(verbose_name="Comment", blank=True) attachments = ArrayField(models.URLField(), size=10, blank=True, default=list) diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 1621b19ea..896681808 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -68,6 +68,7 @@ class User(AbstractBaseUser, PermissionsMixin): last_workspace_id = models.UUIDField(null=True) my_issues_prop = models.JSONField(null=True) role = models.CharField(max_length=300, null=True, blank=True) + is_bot = models.BooleanField(default=False) USERNAME_FIELD = "email" @@ -101,7 +102,7 @@ class User(AbstractBaseUser, PermissionsMixin): @receiver(post_save, sender=User) def send_welcome_email(sender, instance, created, **kwargs): try: - if created: + if created and not instance.is_bot: first_name = instance.first_name.capitalize() to_email = instance.email from_email_string = f"Team Plane " diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index d86598a84..e14c250b4 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -34,9 +34,7 @@ INSTALLED_APPS = [ "rest_framework_simplejwt.token_blacklist", "corsheaders", "taggit", - "fieldsignals", "django_rq", - "channels", ] MIDDLEWARE = [ diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 68f897997..4d4af9b77 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -66,11 +66,3 @@ RQ_QUEUES = { WEB_URL = "http://localhost:3000" -CHANNEL_LAYERS = { - "default": { - "BACKEND": "channels_redis.core.RedisChannelLayer", - "CONFIG": { - "hosts": [(REDIS_HOST, REDIS_PORT)], - }, - }, -} diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index a61d8a909..c83901484 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -1,11 +1,8 @@ """Production settings and globals.""" -import ssl -from typing import Optional from urllib.parse import urlparse import dj_database_url from urllib.parse import urlparse -from redis.asyncio.connection import Connection, RedisSSLContext import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration @@ -186,64 +183,10 @@ RQ_QUEUES = { } -class CustomSSLConnection(Connection): - def __init__( - self, - ssl_context: Optional[str] = None, - **kwargs, - ): - super().__init__(**kwargs) - self.ssl_context = RedisSSLContext(ssl_context) - - -class RedisSSLContext: - __slots__ = ("context",) - - def __init__( - self, - ssl_context, - ): - self.context = ssl_context - - def get(self): - return self.context - - url = urlparse(os.environ.get("REDIS_URL")) -DOCKERIZED = os.environ.get("DOCKERIZED", False) # Set the variable true if running in docker-compose environment - -if not DOCKERIZED: - - ssl_context = ssl.SSLContext() - ssl_context.check_hostname = False - - CHANNEL_LAYERS = { - "default": { - "BACKEND": "channels_redis.core.RedisChannelLayer", - "CONFIG": { - "hosts": [ - { - "host": url.hostname, - "port": url.port, - "username": url.username, - "password": url.password, - "connection_class": CustomSSLConnection, - "ssl_context": ssl_context, - } - ], - }, - }, - } -else: - CHANNEL_LAYERS = { - "default": { - "BACKEND": "channels_redis.core.RedisChannelLayer", - "CONFIG": { - "hosts": [(os.environ.get("REDIS_URL"))], - }, - }, - } - +DOCKERIZED = os.environ.get( + "DOCKERIZED", False +) # Set the variable true if running in docker-compose environment WEB_URL = os.environ.get("WEB_URL") diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index 429530d94..725f2cd85 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -1,11 +1,8 @@ """Production settings and globals.""" -import ssl -from typing import Optional from urllib.parse import urlparse import dj_database_url from urllib.parse import urlparse -from redis.asyncio.connection import Connection, RedisSSLContext import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration @@ -186,52 +183,5 @@ RQ_QUEUES = { } } -class CustomSSLConnection(Connection): - def __init__( - self, - ssl_context: Optional[str] = None, - **kwargs, - ): - super().__init__(**kwargs) - self.ssl_context = RedisSSLContext(ssl_context) - -class RedisSSLContext: - __slots__ = ( - "context", - ) - - def __init__( - self, - ssl_context, - ): - self.context = ssl_context - - def get(self): - return self.context - - -url = urlparse(os.environ.get("REDIS_URL")) - -ssl_context = ssl.SSLContext() -ssl_context.check_hostname = False - -CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - 'hosts': [ - { - 'host': url.hostname, - 'port': url.port, - 'username': url.username, - 'password': url.password, - 'connection_class': CustomSSLConnection, - 'ssl_context': ssl_context, - } - ], - } - }, -} - WEB_URL = os.environ.get("WEB_URL") diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 7dff0a765..578235003 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -20,12 +20,9 @@ sentry-sdk==1.13.0 django-s3-storage==0.13.6 django-crum==0.7.9 django-guardian==2.4.0 -django-fieldsignals==0.7.0 dj_rest_auth==2.2.5 google-auth==2.9.1 google-api-python-client==2.55.0 django-rq==2.5.1 django-redis==5.2.0 -channels==4.0.0 -channels-redis==4.0.0 uvicorn==0.20.0 \ No newline at end of file diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index ff5f8632d..03f9ea822 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -5,6 +5,7 @@ import { CheckCircleIcon } from "@heroicons/react/20/solid"; import { Button, Input } from "components/ui"; // services import authenticationService from "services/authentication.service"; +import useToast from "hooks/use-toast"; // icons // types @@ -16,6 +17,7 @@ type EmailCodeFormValues = { export const EmailCodeForm = ({ onSuccess }: any) => { const [codeSent, setCodeSent] = useState(false); + const { setToastAlert } = useToast(); const { register, handleSubmit, @@ -53,6 +55,11 @@ export const EmailCodeForm = ({ onSuccess }: any) => { }) .catch((error) => { console.log(error); + setToastAlert({ + title: "Oops!", + type: "error", + message: "Enter the correct code to sign in", + }); setError("token" as keyof EmailCodeFormValues, { type: "manual", message: error.error, diff --git a/apps/app/components/account/email-password-form.tsx b/apps/app/components/account/email-password-form.tsx index 029485b8b..613077ac3 100644 --- a/apps/app/components/account/email-password-form.tsx +++ b/apps/app/components/account/email-password-form.tsx @@ -6,6 +6,7 @@ import { useForm } from "react-hook-form"; // ui import { Button, Input } from "components/ui"; import authenticationService from "services/authentication.service"; +import useToast from "hooks/use-toast"; // types type EmailPasswordFormValues = { @@ -15,6 +16,7 @@ type EmailPasswordFormValues = { }; export const EmailPasswordForm = ({ onSuccess }: any) => { + const { setToastAlert } = useToast(); const { register, handleSubmit, @@ -38,6 +40,11 @@ export const EmailPasswordForm = ({ onSuccess }: any) => { }) .catch((error) => { console.log(error); + setToastAlert({ + title: "Oops!", + type: "error", + message: "Enter the correct email address and password to sign in", + }); if (!error?.response?.data) return; Object.keys(error.response.data).forEach((key) => { const err = error.response.data[key]; diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/index.tsx index c4deb7d5b..865bca82d 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/index.tsx @@ -97,49 +97,56 @@ const CommandPalette: React.FC = () => { const handleKeyDown = useCallback( (e: KeyboardEvent) => { - if ((e.ctrlKey || e.metaKey) && e.key === "/") { - e.preventDefault(); - setIsPaletteOpen(true); - } else if ((e.ctrlKey || e.metaKey) && e.key === "i") { - e.preventDefault(); - setIsIssueModalOpen(true); - } else if ((e.ctrlKey || e.metaKey) && e.key === "p") { - e.preventDefault(); - setIsProjectModalOpen(true); - } else if ((e.ctrlKey || e.metaKey) && e.key === "b") { - e.preventDefault(); - toggleCollapsed(); - } else if ((e.ctrlKey || e.metaKey) && e.key === "h") { - e.preventDefault(); - setIsShortcutsModalOpen(true); - } else if ((e.ctrlKey || e.metaKey) && e.key === "q") { - e.preventDefault(); - setIsCreateCycleModalOpen(true); - } else if ((e.ctrlKey || e.metaKey) && e.key === "m") { - e.preventDefault(); - setIsCreateModuleModalOpen(true); - } else if ((e.ctrlKey || e.metaKey) && e.key === "d") { - e.preventDefault(); - setIsBulkDeleteIssuesModalOpen(true); - } else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") { - e.preventDefault(); + if ( + !(e.target instanceof HTMLTextAreaElement) && + !(e.target instanceof HTMLInputElement) && + !(e.target as Element).classList?.contains("remirror-editor") + ) { + if ((e.ctrlKey || e.metaKey) && e.key === "k") { + e.preventDefault(); + setIsPaletteOpen(true); + } else if (e.ctrlKey && e.key === "c") { + console.log("Text copied"); + } else if (e.key === "c") { + e.preventDefault(); + setIsIssueModalOpen(true); + } else if (e.key === "p") { + e.preventDefault(); + setIsProjectModalOpen(true); + } else if ((e.ctrlKey || e.metaKey) && e.key === "b") { + e.preventDefault(); + toggleCollapsed(); + } else if (e.key === "h") { + e.preventDefault(); + setIsShortcutsModalOpen(true); + } else if (e.key === "q") { + e.preventDefault(); + setIsCreateCycleModalOpen(true); + } else if (e.key === "m") { + e.preventDefault(); + setIsCreateModuleModalOpen(true); + } else if (e.key === "Delete") { + e.preventDefault(); + setIsBulkDeleteIssuesModalOpen(true); + } else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") { + e.preventDefault(); + if (!router.query.issueId) return; - if (!router.query.issueId) return; - - const url = new URL(window.location.href); - copyTextToClipboard(url.href) - .then(() => { - setToastAlert({ - type: "success", - title: "Copied to clipboard", + const url = new URL(window.location.href); + copyTextToClipboard(url.href) + .then(() => { + setToastAlert({ + type: "success", + title: "Copied to clipboard", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Some error occurred", + }); }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Some error occurred", - }); - }); + } } }, [toggleCollapsed, setToastAlert, router] diff --git a/apps/app/components/command-palette/shortcuts.tsx b/apps/app/components/command-palette/shortcuts.tsx index 39dc7bcff..d073b7076 100644 --- a/apps/app/components/command-palette/shortcuts.tsx +++ b/apps/app/components/command-palette/shortcuts.tsx @@ -15,7 +15,7 @@ const shortcuts = [ { title: "Navigation", shortcuts: [ - { keys: "ctrl,/", description: "To open navigator" }, + { keys: "ctrl,cmd,k", description: "To open navigator" }, { keys: "↑", description: "Move up" }, { keys: "↓", description: "Move down" }, { keys: "←", description: "Move left" }, @@ -27,14 +27,14 @@ const shortcuts = [ { title: "Common", shortcuts: [ - { keys: "ctrl,p", description: "To create project" }, - { keys: "ctrl,i", description: "To create issue" }, - { keys: "ctrl,q", description: "To create cycle" }, - { keys: "ctrl,m", description: "To create module" }, - { keys: "ctrl,d", description: "To bulk delete issues" }, - { keys: "ctrl,h", description: "To open shortcuts guide" }, + { keys: "p", description: "To create project" }, + { keys: "c", description: "To create issue" }, + { keys: "q", description: "To create cycle" }, + { keys: "m", description: "To create module" }, + { keys: "Delete", description: "To bulk delete issues" }, + { keys: "h", description: "To open shortcuts guide" }, { - keys: "ctrl,alt,c", + keys: "ctrl,cmd,alt,c", description: "To copy issue url when on issue detail page.", }, ], diff --git a/apps/app/components/common/board-view/single-issue.tsx b/apps/app/components/common/board-view/single-issue.tsx index dc774add5..a099c29b1 100644 --- a/apps/app/components/common/board-view/single-issue.tsx +++ b/apps/app/components/common/board-view/single-issue.tsx @@ -1,64 +1,68 @@ import React from "react"; -// next + import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; -// swr -import useSWR from "swr"; + +import useSWR, { mutate } from "swr"; + // react-beautiful-dnd import { DraggableStateSnapshot } from "react-beautiful-dnd"; +// react-datepicker +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // constants import { TrashIcon } from "@heroicons/react/24/outline"; -import { CalendarDaysIcon } from "@heroicons/react/20/solid"; // services import issuesService from "services/issues.service"; import stateService from "services/state.service"; import projectService from "services/project.service"; // components -import { AssigneesList } from "components/ui/avatar"; +import { AssigneesList, CustomDatePicker } from "components/ui"; // helpers import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IssueResponse, IUserLite, IWorkspaceMember, Properties } from "types"; +import { IIssue, IUserLite, IWorkspaceMember, Properties, UserAuth } from "types"; // common import { PRIORITIES } from "constants/"; -import { PROJECT_ISSUES_LIST, STATE_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; +import { + STATE_LIST, + PROJECT_DETAILS, + CYCLE_ISSUES, + MODULE_ISSUES, + PROJECT_ISSUES_LIST, +} from "constants/fetch-keys"; import { getPriorityIcon } from "constants/global"; type Props = { + type?: string; + typeId?: string; issue: IIssue; properties: Properties; snapshot?: DraggableStateSnapshot; assignees: Partial[] | (Partial | undefined)[]; people: IWorkspaceMember[] | undefined; handleDeleteIssue?: React.Dispatch>; - partialUpdateIssue: any; + userAuth: UserAuth; }; const SingleBoardIssue: React.FC = ({ + type, + typeId, issue, properties, snapshot, assignees, people, handleDeleteIssue, - partialUpdateIssue, + userAuth, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: issues } = useSWR( - workspaceSlug && projectId - ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) - : null, - workspaceSlug && projectId - ? () => issuesService.getIssues(workspaceSlug as string, projectId as string) - : null - ); - const { data: states } = useSWR( workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug && projectId @@ -73,7 +77,25 @@ const SingleBoardIssue: React.FC = ({ : null ); - const totalChildren = issues?.results.filter((i) => i.parent === issue.id).length; + const partialUpdateIssue = (formData: Partial) => { + if (!workspaceSlug || !projectId) return; + + issuesService + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData) + .then((res) => { + if (typeId) { + mutate(CYCLE_ISSUES(typeId ?? "")); + mutate(MODULE_ISSUES(typeId ?? "")); + } + + mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); + }) + .catch((error) => { + console.log(error); + }); + }; + + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return (
= ({ }`} >
- {handleDeleteIssue && ( + {handleDeleteIssue && !isNotAllowed && (
)} diff --git a/apps/app/components/common/existing-issues-list-modal.tsx b/apps/app/components/common/existing-issues-list-modal.tsx index e4ffa9896..5179facd4 100644 --- a/apps/app/components/common/existing-issues-list-modal.tsx +++ b/apps/app/components/common/existing-issues-list-modal.tsx @@ -77,11 +77,18 @@ const ExistingIssuesListModal: React.FC = ({ type: "error", message: "Please select atleast one issue", }); + return; } await handleOnSubmit(data); handleClose(); + + setToastAlert({ + title: "Success", + type: "success", + message: `Issue${data.issues.length > 1 ? "s" : ""} added successfully`, + }); }; const filteredIssues: IIssue[] = @@ -182,10 +189,7 @@ const ExistingIssuesListModal: React.FC = ({

No issues found. Create a new issue with{" "} -
-                                  Ctrl/Command + I
-                                
- . +
C
.

)} diff --git a/apps/app/components/common/list-view/single-issue.tsx b/apps/app/components/common/list-view/single-issue.tsx index 907282edb..8879b9fb7 100644 --- a/apps/app/components/common/list-view/single-issue.tsx +++ b/apps/app/components/common/list-view/single-issue.tsx @@ -5,6 +5,9 @@ import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; +// react-datepicker +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; // services import issuesService from "services/issues.service"; import workspaceService from "services/workspace.service"; @@ -12,7 +15,7 @@ import stateService from "services/state.service"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // ui -import { CustomMenu, CustomSelect, AssigneesList, Avatar } from "components/ui"; +import { CustomMenu, CustomSelect, AssigneesList, Avatar, CustomDatePicker } from "components/ui"; // components import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion"; // icons @@ -21,7 +24,7 @@ import { CalendarDaysIcon } from "@heroicons/react/24/outline"; import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IWorkspaceMember, Properties } from "types"; +import { IIssue, IWorkspaceMember, Properties, UserAuth } from "types"; // fetch-keys import { CYCLE_ISSUES, @@ -41,6 +44,7 @@ type Props = { properties: Properties; editIssue: () => void; removeIssue?: () => void; + userAuth: UserAuth; }; const SingleListIssue: React.FC = ({ @@ -50,6 +54,7 @@ const SingleListIssue: React.FC = ({ properties, editIssue, removeIssue, + userAuth, }) => { const [deleteIssue, setDeleteIssue] = useState(); @@ -86,6 +91,8 @@ const SingleListIssue: React.FC = ({ }); }; + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; + return ( <> = ({ partialUpdateIssue({ priority: data }); }} className="group relative flex-shrink-0" + disabled={isNotAllowed} > {({ open }) => ( <>
= ({ }} maxHeight="md" noChevron + disabled={isNotAllowed} > {states?.map((state) => ( @@ -226,9 +237,14 @@ const SingleListIssue: React.FC = ({ ))} )} + {/* {properties.cycle && !typeId && ( +
+ {issue.issue_cycle ? issue.issue_cycle.cycle_detail.name : "None"} +
+ )} */} {properties.due_date && (
= ({ : findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400" }`} > - - {issue.target_date ? renderShortNumericDateFormat(issue.target_date) : "N/A"} + { + partialUpdateIssue({ + target_date: val + ? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}` + : null, + }); + }} + className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"} + /> + {/* { + partialUpdateIssue({ + target_date: val + ? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}` + : null, + }); + }} + dateFormat="dd-MM-yyyy" + className={`cursor-pointer rounded-md border px-2 py-[3px] text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${ + issue?.target_date ? "w-[4.5rem]" : "w-[3rem] text-center" + }`} + isClearable + /> */}
Due date
{renderShortNumericDateFormat(issue.target_date ?? "")}
@@ -253,7 +298,7 @@ const SingleListIssue: React.FC = ({
)} {properties.sub_issue_count && ( -
+
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
)} @@ -270,12 +315,17 @@ const SingleListIssue: React.FC = ({ partialUpdateIssue({ assignees_list: newData }); }} className="group relative flex-shrink-0" + disabled={isNotAllowed} > {({ open }) => ( <>
-
+
@@ -325,7 +375,7 @@ const SingleListIssue: React.FC = ({ )} )} - {type && ( + {type && !isNotAllowed && ( Edit {type !== "issue" && ( diff --git a/apps/app/components/issues/description-form.tsx b/apps/app/components/issues/description-form.tsx index 441efedc5..9882eee36 100644 --- a/apps/app/components/issues/description-form.tsx +++ b/apps/app/components/issues/description-form.tsx @@ -1,7 +1,11 @@ -import { FC, useEffect, useState } from "react"; +import { FC, useCallback, useEffect, useMemo } from "react"; + import dynamic from "next/dynamic"; -// types -import { IIssue } from "types"; + +// react-hook-form +import { useForm } from "react-hook-form"; +// lodash +import debounce from "lodash.debounce"; // components import { Loader, Input } from "components/ui"; const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { @@ -12,8 +16,9 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor ), }); -// hooks -import useDebounce from "hooks/use-debounce"; +// types +import { IIssue } from "types"; +import useToast from "hooks/use-toast"; export interface IssueDescriptionFormValues { name: string; @@ -23,32 +28,74 @@ export interface IssueDescriptionFormValues { export interface IssueDetailsProps { issue: IIssue; - handleSubmit: (value: IssueDescriptionFormValues) => void; + handleFormSubmit: (value: IssueDescriptionFormValues) => void; } -export const IssueDescriptionForm: FC = ({ issue, handleSubmit }) => { - // states - // const [issueFormValues, setIssueFormValues] = useState({ - // name: issue.name, - // description: issue?.description, - // description_html: issue?.description_html, - // }); +export const IssueDescriptionForm: FC = ({ issue, handleFormSubmit }) => { + const { setToastAlert } = useToast(); - const [issueName, setIssueName] = useState(issue?.name); - const [issueDescription, setIssueDescription] = useState(issue?.description); - const [issueDescriptionHTML, setIssueDescriptionHTML] = useState(issue?.description_html); + const { + handleSubmit, + watch, + setValue, + reset, + formState: { errors }, + setError, + } = useForm({ + defaultValues: { + name: "", + description: "", + description_html: "", + }, + }); - // hooks - const formValues = useDebounce( - { name: issueName, description: issueDescription, description_html: issueDescriptionHTML }, - 2000 + const handleDescriptionFormSubmit = useCallback( + (formData: Partial) => { + if (!formData.name || formData.name === "") { + setToastAlert({ + type: "error", + title: "Error in saving!", + message: "Title is required.", + }); + return; + } + + if (formData.name.length > 255) { + setToastAlert({ + type: "error", + title: "Error in saving!", + message: "Title cannot have more than 255 characters.", + }); + return; + } + + handleFormSubmit({ + name: formData.name ?? "", + description: formData.description, + description_html: formData.description_html, + }); + }, + [handleFormSubmit, setToastAlert] ); - const stringFromValues = JSON.stringify(formValues); + const debounceHandler = useMemo( + () => debounce(handleSubmit(handleDescriptionFormSubmit), 2000), + [handleSubmit, handleDescriptionFormSubmit] + ); + + useEffect( + () => () => { + debounceHandler.cancel(); + }, + [debounceHandler] + ); + + // reset form values useEffect(() => { - handleSubmit(formValues); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handleSubmit, stringFromValues]); + if (!issue) return; + + reset(issue); + }, [issue, reset]); return (
@@ -56,18 +103,24 @@ export const IssueDescriptionForm: FC = ({ issue, handleSubmi id="name" placeholder="Enter issue name" name="name" + value={watch("name")} autoComplete="off" - value={issueName} - onChange={(e) => setIssueName(e.target.value)} + onChange={(e) => { + setValue("name", e.target.value); + debounceHandler(); + }} mode="transparent" className="text-xl font-medium" - required={true} /> + {errors.name ? errors.name.message : null} setIssueDescription(json)} - onHTMLChange={(html) => setIssueDescriptionHTML(html)} + value={watch("description")} + placeholder="Describe the issue..." + onJSONChange={(json) => { + setValue("description", json); + debounceHandler(); + }} + onHTMLChange={(html) => setValue("description_html", html)} />
); diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index c85872ee1..ae454afb3 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -19,7 +19,7 @@ import { CycleSelect as IssueCycleSelect } from "components/cycles/select"; import CreateUpdateStateModal from "components/project/issues/BoardView/state/create-update-state-modal"; import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; // ui -import { Button, CustomMenu, Input, Loader } from "components/ui"; +import { Button, CustomDatePicker, CustomMenu, Input, Loader } from "components/ui"; // icons import { XMarkIcon } from "@heroicons/react/24/outline"; // helpers @@ -194,10 +194,10 @@ export const IssueForm: FC = ({ error={errors.name} register={register} validations={{ - required: "Name is required", + required: "Title is required", maxLength: { value: 255, - message: "Name should be less than 255 characters", + message: "Title should be less than 255 characters", }, }} /> @@ -289,20 +289,25 @@ export const IssueForm: FC = ({ )} /> - ( - { - onChange(e.target.value); - }} - className="cursor-pointer rounded-md border px-2 py-[3px] text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" - /> - )} - /> +
+ ( + { + onChange( + val + ? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}` + : null + ); + }} + className="max-w-[7rem]" + /> + )} + /> +
= ({ setToastAlert({ title: "Success", type: "success", - message: `Issue ${data ? "updated" : "created"} successfully`, + message: "Issue created successfully", }); if (payload.assignees_list?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE); diff --git a/apps/app/components/onboarding/break-into-modules.tsx b/apps/app/components/onboarding/break-into-modules.tsx index a87e2d0ce..e9f8c170b 100644 --- a/apps/app/components/onboarding/break-into-modules.tsx +++ b/apps/app/components/onboarding/break-into-modules.tsx @@ -4,30 +4,30 @@ import Image from "next/image"; import Module from "public/onboarding/module.png"; const BreakIntoModules: React.FC = () => ( -
-
-
- Plane- Modules -
-
-

Break into Modules

-

- Modules break your big think into Projects or Features, to help you organize better. -

-

4/5

-
+
+
+
+ Plane- Modules
- ); +
+

Break into Modules

+

+ Modules break your big thing into Projects or Features, to help you organize better. +

+

4/5

+
+
+); export default BreakIntoModules; diff --git a/apps/app/components/project/card.tsx b/apps/app/components/project/card.tsx index 4ef975128..dd6d96e32 100644 --- a/apps/app/components/project/card.tsx +++ b/apps/app/components/project/card.tsx @@ -46,14 +46,16 @@ export const ProjectCard: React.FC = (props) => { <>
-
+ diff --git a/apps/app/components/project/confirm-project-deletion.tsx b/apps/app/components/project/confirm-project-deletion.tsx index a55df3805..236f71bdf 100644 --- a/apps/app/components/project/confirm-project-deletion.tsx +++ b/apps/app/components/project/confirm-project-deletion.tsx @@ -2,19 +2,19 @@ import React, { useEffect, useRef, useState } from "react"; import { mutate } from "swr"; +// headless ui import { Dialog, Transition } from "@headlessui/react"; - // services -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import type { IProject, IWorkspace } from "types"; import projectService from "services/project.service"; // hooks import useToast from "hooks/use-toast"; // icons +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui import { Button, Input } from "components/ui"; // types -// constants +import type { IProject, IWorkspace } from "types"; +// fetch-keys import { PROJECTS_LIST } from "constants/fetch-keys"; type TConfirmProjectDeletionProps = { @@ -86,7 +86,7 @@ const ConfirmProjectDeletion: React.FC = (props) = @@ -102,7 +102,7 @@ const ConfirmProjectDeletion: React.FC = (props) =
-
+
= ({ isOpen, onClose, data, ha @@ -49,7 +49,7 @@ const ConfirmProjectMemberRemove: React.FC = ({ isOpen, onClose, data, ha
-
+
= (props) => { const projectIdentifier = watch("identifier") ?? ""; useEffect(() => { - if (projectName && isChangeIdentifierRequired) { + if (projectName && isChangeIdentifierRequired) setValue("identifier", projectName.replace(/ /g, "").toUpperCase().substring(0, 3)); - } }, [projectName, projectIdentifier, setValue, isChangeIdentifierRequired]); useEffect(() => () => setIsChangeIdentifierRequired(true), [isOpen]); @@ -185,7 +184,7 @@ export const CreateProjectModal: React.FC = (props) => {

-
+
diff --git a/apps/app/components/project/cycles/board-view/index.tsx b/apps/app/components/project/cycles/board-view/index.tsx index 901fb03db..ee37b9c75 100644 --- a/apps/app/components/project/cycles/board-view/index.tsx +++ b/apps/app/components/project/cycles/board-view/index.tsx @@ -13,7 +13,7 @@ import SingleBoard from "components/project/cycles/board-view/single-board"; // ui import { Spinner } from "components/ui"; // types -import { CycleIssueResponse, IIssue, IProjectMember } from "types"; +import { CycleIssueResponse, IIssue, IProjectMember, UserAuth } from "types"; import issuesService from "services/issues.service"; // constants import { STATE_LIST, CYCLE_ISSUES } from "constants/fetch-keys"; @@ -23,8 +23,6 @@ type Props = { members: IProjectMember[] | undefined; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; openIssuesListModal: () => void; - removeIssueFromCycle: (bridgeId: string) => void; - partialUpdateIssue: (formData: Partial, issueId: string) => void; handleDeleteIssue: React.Dispatch>; setPreloadedData: React.Dispatch< React.SetStateAction< @@ -34,6 +32,7 @@ type Props = { | null > >; + userAuth: UserAuth; }; const CyclesBoardView: React.FC = ({ @@ -41,10 +40,9 @@ const CyclesBoardView: React.FC = ({ members, openCreateIssueModal, openIssuesListModal, - removeIssueFromCycle, - partialUpdateIssue, handleDeleteIssue, setPreloadedData, + userAuth, }) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -128,7 +126,7 @@ const CyclesBoardView: React.FC = ({ return ( <> {groupedByIssues ? ( -
+
@@ -151,10 +149,8 @@ const CyclesBoardView: React.FC = ({ : "#000000" } properties={properties} - removeIssueFromCycle={removeIssueFromCycle} openIssuesListModal={openIssuesListModal} openCreateIssueModal={openCreateIssueModal} - partialUpdateIssue={partialUpdateIssue} handleDeleteIssue={handleDeleteIssue} setPreloadedData={setPreloadedData} stateId={ @@ -162,6 +158,7 @@ const CyclesBoardView: React.FC = ({ ? states?.find((s) => s.name === singleGroup)?.id ?? null : null } + userAuth={userAuth} /> ))}
diff --git a/apps/app/components/project/cycles/board-view/single-board.tsx b/apps/app/components/project/cycles/board-view/single-board.tsx index 581b85207..ab028c2cf 100644 --- a/apps/app/components/project/cycles/board-view/single-board.tsx +++ b/apps/app/components/project/cycles/board-view/single-board.tsx @@ -17,7 +17,7 @@ import { CustomMenu } from "components/ui"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; // types -import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types"; +import { IIssue, IWorkspaceMember, NestedKeyOf, Properties, UserAuth } from "types"; // fetch-keys import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; @@ -32,8 +32,6 @@ type Props = { bgColor?: string; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; openIssuesListModal: () => void; - removeIssueFromCycle: (bridgeId: string) => void; - partialUpdateIssue: (formData: Partial, issueId: string) => void; handleDeleteIssue: React.Dispatch>; setPreloadedData: React.Dispatch< React.SetStateAction< @@ -44,6 +42,7 @@ type Props = { > >; stateId: string | null; + userAuth: UserAuth; }; const SingleModuleBoard: React.FC = ({ @@ -55,18 +54,17 @@ const SingleModuleBoard: React.FC = ({ bgColor, openCreateIssueModal, openIssuesListModal, - removeIssueFromCycle, - partialUpdateIssue, handleDeleteIssue, setPreloadedData, stateId, + userAuth, }) => { // collapse/expand const [isCollapsed, setIsCollapsed] = useState(true); const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, cycleId } = router.query; if (selectedGroup === "priority") groupTitle === "high" @@ -132,13 +130,15 @@ const SingleModuleBoard: React.FC = ({ {...provided.dragHandleProps} >
)} diff --git a/apps/app/components/project/cycles/confirm-cycle-deletion.tsx b/apps/app/components/project/cycles/confirm-cycle-deletion.tsx index 977dc8ac0..ab35d343b 100644 --- a/apps/app/components/project/cycles/confirm-cycle-deletion.tsx +++ b/apps/app/components/project/cycles/confirm-cycle-deletion.tsx @@ -5,12 +5,14 @@ import { useRouter } from "next/router"; import { mutate } from "swr"; // headless ui import { Dialog, Transition } from "@headlessui/react"; -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // services import cycleService from "services/cycles.service"; +// hooks +import useToast from "hooks/use-toast"; // ui import { Button } from "components/ui"; // icons +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types import type { ICycle } from "types"; type TConfirmCycleDeletionProps = { @@ -21,15 +23,19 @@ type TConfirmCycleDeletionProps = { // fetch-keys import { CYCLE_LIST } from "constants/fetch-keys"; -const ConfirmCycleDeletion: React.FC = (props) => { - const { isOpen, setIsOpen, data } = props; - +const ConfirmCycleDeletion: React.FC = ({ + isOpen, + setIsOpen, + data, +}) => { const cancelButtonRef = useRef(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); const { workspaceSlug } = router.query; + const { setToastAlert } = useToast(); + useEffect(() => { data && setIsOpen(true); }, [data, setIsOpen]); @@ -51,6 +57,12 @@ const ConfirmCycleDeletion: React.FC = (props) => { false ); handleClose(); + + setToastAlert({ + title: "Success", + type: "success", + message: "Cycle deleted successfully", + }); }) .catch((error) => { console.log(error); diff --git a/apps/app/components/project/cycles/create-update-cycle-modal.tsx b/apps/app/components/project/cycles/create-update-cycle-modal.tsx index 685f6a531..ef0a75cf5 100644 --- a/apps/app/components/project/cycles/create-update-cycle-modal.tsx +++ b/apps/app/components/project/cycles/create-update-cycle-modal.tsx @@ -1,20 +1,23 @@ import React, { useEffect } from "react"; -// next + import { useRouter } from "next/router"; -// swr + import { mutate } from "swr"; + // react hook form import { Controller, useForm } from "react-hook-form"; -// headless +// headless ui import { Dialog, Transition } from "@headlessui/react"; -// types -import type { ICycle } from "types"; // services import cycleService from "services/cycles.service"; -import { Button, Input, TextArea, CustomSelect } from "components/ui"; +// hooks +import useToast from "hooks/use-toast"; // ui +import { Button, Input, TextArea, CustomSelect, CustomDatePicker } from "components/ui"; // common import { renderDateFormat } from "helpers/date-time.helper"; +// types +import type { ICycle } from "types"; // fetch keys import { CYCLE_LIST } from "constants/fetch-keys"; @@ -29,14 +32,16 @@ const defaultValues: Partial = { name: "", description: "", status: "draft", - start_date: new Date().toString(), - end_date: new Date().toString(), + start_date: null, + end_date: null, }; const CreateUpdateCycleModal: React.FC = ({ isOpen, setIsOpen, data, projectId }) => { const router = useRouter(); const { workspaceSlug } = router.query; + const { setToastAlert } = useToast(); + const { register, formState: { errors, isSubmitting }, @@ -69,7 +74,13 @@ const CreateUpdateCycleModal: React.FC = ({ isOpen, setIsOpen, data, proj .createCycle(workspaceSlug as string, projectId, payload) .then((res) => { mutate(CYCLE_LIST(projectId), (prevData) => [res, ...(prevData ?? [])], false); + handleClose(); + setToastAlert({ + title: "Success", + type: "success", + message: "Cycle created successfully", + }); }) .catch((err) => { Object.keys(err).map((key) => { @@ -82,20 +93,14 @@ const CreateUpdateCycleModal: React.FC = ({ isOpen, setIsOpen, data, proj await cycleService .updateCycle(workspaceSlug as string, projectId, data.id, payload) .then((res) => { - mutate( - CYCLE_LIST(projectId), - (prevData) => { - const newData = prevData?.map((item) => { - if (item.id === res.id) { - return res; - } - return item; - }); - return newData; - }, - false - ); + mutate(CYCLE_LIST(projectId)); handleClose(); + + setToastAlert({ + title: "Success", + type: "success", + message: "Cycle updated successfully", + }); }) .catch((err) => { Object.keys(err).map((key) => { @@ -157,6 +162,10 @@ const CreateUpdateCycleModal: React.FC = ({ isOpen, setIsOpen, data, proj register={register} validations={{ required: "Name is required", + maxLength: { + value: 255, + message: "Name should be less than 255 characters", + }, }} />
@@ -198,32 +207,62 @@ const CreateUpdateCycleModal: React.FC = ({ isOpen, setIsOpen, data, proj
- +
Start Date
+
+ ( + { + onChange( + val + ? `${val.getFullYear()}-${ + val.getMonth() + 1 + }-${val.getDate()}` + : null + ); + }} + error={errors.start_date ? true : false} + /> + )} + /> + {errors.start_date && ( +
{errors.start_date.message}
+ )} +
- +
End Date
+
+ ( + { + onChange( + val + ? `${val.getFullYear()}-${ + val.getMonth() + 1 + }-${val.getDate()}` + : null + ); + }} + error={errors.end_date ? true : false} + /> + )} + /> + {errors.end_date && ( +
{errors.end_date.message}
+ )} +
diff --git a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx index 08ef636dd..dd81f0692 100644 --- a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx +++ b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx @@ -13,14 +13,17 @@ import cyclesService from "services/cycles.service"; // hooks import useToast from "hooks/use-toast"; // ui -import { Loader } from "components/ui"; +import { Loader, CustomDatePicker } from "components/ui"; +//progress-bar +import { CircularProgressbar } from "react-circular-progressbar"; +import "react-circular-progressbar/dist/styles.css"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { groupBy } from "helpers/array.helper"; // types import { CycleIssueResponse, ICycle } from "types"; // fetch-keys -import { CYCLE_DETAIL } from "constants/fetch-keys"; +import { CYCLE_LIST } from "constants/fetch-keys"; type Props = { cycle: ICycle | undefined; @@ -35,9 +38,7 @@ const defaultValues: Partial = { const CycleDetailSidebar: React.FC = ({ cycle, isOpen, cycleIssues }) => { const router = useRouter(); - const { - query: { workspaceSlug, projectId }, - } = router; + const { workspaceSlug, projectId, cycleId } = router.query; const { setToastAlert } = useToast(); @@ -57,11 +58,21 @@ const CycleDetailSidebar: React.FC = ({ cycle, isOpen, cycleIssues }) => const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !module) return; + mutate( + projectId && CYCLE_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((tempCycle) => { + if (tempCycle.id === cycleId) return { ...tempCycle, ...data }; + return tempCycle; + }), + false + ); + cyclesService .patchCycle(workspaceSlug as string, projectId as string, cycle?.id ?? "", data) .then((res) => { console.log(res); - mutate(CYCLE_DETAIL); + mutate(CYCLE_LIST(projectId as string)); }) .catch((e) => { console.log(e); @@ -135,7 +146,13 @@ const CycleDetailSidebar: React.FC = ({ cycle, isOpen, cycleIssues }) =>
- + + +
{groupedIssues.completed.length}/{cycleIssues?.length}
@@ -151,16 +168,17 @@ const CycleDetailSidebar: React.FC = ({ cycle, isOpen, cycleIssues }) => ( - { - submitChanges({ start_date: e.target.value }); - onChange(e.target.value); + render={({ field: { value } }) => ( + { + submitChanges({ + start_date: val + ? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}` + : null, + }); }} - className="w-full cursor-pointer rounded-md border bg-transparent px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" + isClearable={false} /> )} /> @@ -175,16 +193,17 @@ const CycleDetailSidebar: React.FC = ({ cycle, isOpen, cycleIssues }) => ( - { - submitChanges({ end_date: e.target.value }); - onChange(e.target.value); + render={({ field: { value } }) => ( + { + submitChanges({ + end_date: val + ? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}` + : null, + }); }} - className="w-full cursor-pointer rounded-md border bg-transparent px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" + isClearable={false} /> )} /> diff --git a/apps/app/components/project/cycles/list-view/index.tsx b/apps/app/components/project/cycles/list-view/index.tsx index feb22fd46..e86ec8de4 100644 --- a/apps/app/components/project/cycles/list-view/index.tsx +++ b/apps/app/components/project/cycles/list-view/index.tsx @@ -21,7 +21,7 @@ import { CustomMenu, Spinner } from "components/ui"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IWorkspaceMember } from "types"; +import { IIssue, IWorkspaceMember, UserAuth } from "types"; // fetch-keys import { WORKSPACE_MEMBERS, STATE_LIST } from "constants/fetch-keys"; @@ -38,6 +38,7 @@ type Props = { | null > >; + userAuth: UserAuth; }; const CyclesListView: React.FC = ({ @@ -46,6 +47,7 @@ const CyclesListView: React.FC = ({ openIssuesListModal, removeIssueFromCycle, setPreloadedData, + userAuth, }) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -140,6 +142,7 @@ const CyclesListView: React.FC = ({ properties={properties} editIssue={() => openCreateIssueModal(issue, "edit")} removeIssue={() => removeIssueFromCycle(issue.bridge ?? "")} + userAuth={userAuth} /> ); }) diff --git a/apps/app/components/project/cycles/stats-view/index.tsx b/apps/app/components/project/cycles/stats-view/index.tsx index 4c7ea59ba..58f316464 100644 --- a/apps/app/components/project/cycles/stats-view/index.tsx +++ b/apps/app/components/project/cycles/stats-view/index.tsx @@ -64,7 +64,7 @@ const CycleStatsView: React.FC = ({ )}

No {type} {type === "current" ? "cycle" : "cycles"} yet. Create with{" "} -
Ctrl/Command + Q
. +
Q
.

)} diff --git a/apps/app/components/project/cycles/stats-view/single-stat.tsx b/apps/app/components/project/cycles/stats-view/single-stat.tsx index 8dae743cf..886735d85 100644 --- a/apps/app/components/project/cycles/stats-view/single-stat.tsx +++ b/apps/app/components/project/cycles/stats-view/single-stat.tsx @@ -71,7 +71,9 @@ const SingleStat: React.FC = (props) => {
-

{cycle.name}

+

+ {cycle.name} +

diff --git a/apps/app/components/project/issues/BoardView/index.tsx b/apps/app/components/project/issues/BoardView/index.tsx index b1936192f..6243e88dd 100644 --- a/apps/app/components/project/issues/BoardView/index.tsx +++ b/apps/app/components/project/issues/BoardView/index.tsx @@ -21,17 +21,17 @@ import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deleti // ui import { Spinner } from "components/ui"; // types -import type { IState, IIssue, IssueResponse } from "types"; +import type { IState, IIssue, IssueResponse, UserAuth } from "types"; // fetch-keys import { STATE_LIST, PROJECT_ISSUES_LIST, PROJECT_MEMBERS } from "constants/fetch-keys"; type Props = { issues: IIssue[]; handleDeleteIssue: React.Dispatch>; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + userAuth: UserAuth; }; -const BoardView: React.FC = ({ issues, handleDeleteIssue, partialUpdateIssue }) => { +const BoardView: React.FC = ({ issues, handleDeleteIssue, userAuth }) => { const [createIssueModal, setCreateIssueModal] = useState(false); const [isIssueDeletionOpen, setIsIssueDeletionOpen] = useState(false); const [issueDeletionData, setIssueDeletionData] = useState(); @@ -68,7 +68,8 @@ const BoardView: React.FC = ({ issues, handleDeleteIssue, partialUpdateIs const handleOnDragEnd = useCallback( (result: DropResult) => { - if (!result.destination) return; + if (!result.destination || !workspaceSlug || !projectId) return; + const { source, destination, type } = result; if (destination.droppableId === "trashBox") { @@ -94,7 +95,7 @@ const BoardView: React.FC = ({ issues, handleDeleteIssue, partialUpdateIs newStates[destination.index].sequence = sequenceNumber; mutateState(newStates, false); - if (!workspaceSlug) return; + stateServices .patchState( workspaceSlug as string, @@ -140,18 +141,6 @@ const BoardView: React.FC = ({ issues, handleDeleteIssue, partialUpdateIs draggedItem.state = destinationStateId; draggedItem.state_detail = destinationState; - // patch request - issuesServices.patchIssue( - workspaceSlug as string, - projectId as string, - draggedItem.id, - { - state: destinationStateId, - } - ); - - // mutate the issues - if (!workspaceSlug || !projectId) return; mutate( PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), (prevData) => { @@ -175,6 +164,15 @@ const BoardView: React.FC = ({ issues, handleDeleteIssue, partialUpdateIs }, false ); + + // patch request + issuesServices + .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { + state: destinationStateId, + }) + .then((res) => { + mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); + }); } } } @@ -200,7 +198,7 @@ const BoardView: React.FC = ({ issues, handleDeleteIssue, partialUpdateIs }} /> {groupedByIssues ? ( -
+
@@ -238,7 +236,7 @@ const BoardView: React.FC = ({ issues, handleDeleteIssue, partialUpdateIs : "#000000" } handleDeleteIssue={handleDeleteIssue} - partialUpdateIssue={partialUpdateIssue} + userAuth={userAuth} /> ))}
diff --git a/apps/app/components/project/issues/BoardView/single-board.tsx b/apps/app/components/project/issues/BoardView/single-board.tsx index 68c4e640a..8630f223c 100644 --- a/apps/app/components/project/issues/BoardView/single-board.tsx +++ b/apps/app/components/project/issues/BoardView/single-board.tsx @@ -15,7 +15,7 @@ import { PlusIcon } from "@heroicons/react/24/outline"; // services import workspaceService from "services/workspace.service"; // types -import { IIssue, Properties, NestedKeyOf, IWorkspaceMember } from "types"; +import { IIssue, Properties, NestedKeyOf, IWorkspaceMember, UserAuth } from "types"; // fetch-keys import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; @@ -40,7 +40,7 @@ type Props = { stateId: string | null; createdBy: string | null; handleDeleteIssue: React.Dispatch>; - partialUpdateIssue: (formData: Partial, childIssueId: string) => void; + userAuth: UserAuth; }; const SingleBoard: React.FC = ({ @@ -55,7 +55,7 @@ const SingleBoard: React.FC = ({ stateId, createdBy, handleDeleteIssue, - partialUpdateIssue, + userAuth, }) => { // Collapse/Expand const [isCollapsed, setIsCollapsed] = useState(true); @@ -145,7 +145,7 @@ const SingleBoard: React.FC = ({ people={people} assignees={assignees} handleDeleteIssue={handleDeleteIssue} - partialUpdateIssue={partialUpdateIssue} + userAuth={userAuth} />
)} diff --git a/apps/app/components/project/issues/BoardView/state/confirm-state-delete.tsx b/apps/app/components/project/issues/BoardView/state/confirm-state-delete.tsx index 3cf8e4075..ecf3e7874 100644 --- a/apps/app/components/project/issues/BoardView/state/confirm-state-delete.tsx +++ b/apps/app/components/project/issues/BoardView/state/confirm-state-delete.tsx @@ -8,16 +8,18 @@ import useSWR, { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -// types -import type { IState } from "types"; // services import stateServices from "services/state.service"; import issuesServices from "services/issues.service"; +// hooks +import useToast from "hooks/use-toast"; // ui import { Button } from "components/ui"; // helpers import { groupBy } from "helpers/array.helper"; -// fetch api +// types +import type { IState } from "types"; +// fetch-keys import { STATE_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; type Props = { @@ -33,6 +35,8 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, onClose, data }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { setToastAlert } = useToast(); + const { data: issues } = useSWR( workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) @@ -61,6 +65,12 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, onClose, data }) => { false ); handleClose(); + + setToastAlert({ + title: "Success", + type: "success", + message: "State deleted successfully", + }); }) .catch((error) => { console.log(error); @@ -78,7 +88,7 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, onClose, data }) => { @@ -94,7 +104,7 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, onClose, data }) => {
-
+
= ({ onClose, selectedGroup, }) => { + const { setToastAlert } = useToast(); + const { register, handleSubmit, @@ -81,6 +87,12 @@ export const CreateUpdateStateInline: React.FC = ({ .then((res) => { mutate(STATE_LIST(projectId), (prevData) => [...(prevData ?? []), res]); handleClose(); + + setToastAlert({ + title: "Success", + type: "success", + message: "State created successfully", + }); }) .catch((err) => { Object.keys(err).map((key) => { @@ -95,16 +107,14 @@ export const CreateUpdateStateInline: React.FC = ({ ...payload, }) .then((res) => { - mutate(STATE_LIST(projectId), (prevData) => { - const newData = prevData?.map((item) => { - if (item.id === res.id) { - return res; - } - return item; - }); - return newData; - }); + mutate(STATE_LIST(projectId)); handleClose(); + + setToastAlert({ + title: "Success", + type: "success", + message: "State updated successfully", + }); }) .catch((err) => { Object.keys(err).map((key) => { @@ -173,18 +183,27 @@ export const CreateUpdateStateInline: React.FC = ({ autoComplete="off" /> {data && ( - = ({ Cancel ); diff --git a/apps/app/components/project/issues/confirm-issue-deletion.tsx b/apps/app/components/project/issues/confirm-issue-deletion.tsx index a9f689543..38d2258ce 100644 --- a/apps/app/components/project/issues/confirm-issue-deletion.tsx +++ b/apps/app/components/project/issues/confirm-issue-deletion.tsx @@ -3,20 +3,21 @@ import React, { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; + // headless ui import { Dialog, Transition } from "@headlessui/react"; -// fetching keys -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import type { CycleIssueResponse, IIssue, IssueResponse, ModuleIssueResponse } from "types"; -import { CYCLE_ISSUES, PROJECT_ISSUES_LIST, MODULE_ISSUES } from "constants/fetch-keys"; // services import issueServices from "services/issues.service"; // hooks import useToast from "hooks/use-toast"; // icons +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui import { Button } from "components/ui"; // types +import type { CycleIssueResponse, IIssue, IssueResponse, ModuleIssueResponse } from "types"; +// fetch-keys +import { CYCLE_ISSUES, PROJECT_ISSUES_LIST, MODULE_ISSUES } from "constants/fetch-keys"; type Props = { isOpen: boolean; @@ -79,12 +80,12 @@ const ConfirmIssueDeletion: React.FC = (props) => { ); } + handleClose(); setToastAlert({ title: "Success", type: "success", message: "Issue deleted successfully", }); - handleClose(); }) .catch((error) => { console.log(error); diff --git a/apps/app/components/project/issues/issue-detail/activity/index.tsx b/apps/app/components/project/issues/issue-detail/activity/index.tsx index 3b11436e3..3594bf7a8 100644 --- a/apps/app/components/project/issues/issue-detail/activity/index.tsx +++ b/apps/app/components/project/issues/issue-detail/activity/index.tsx @@ -76,11 +76,6 @@ const activityDetails: { }, }; -const defaultValues: Partial = { - comment_html: "", - comment_json: "", -}; - const IssueActivitySection: React.FC<{ issueActivities: IIssueActivity[]; mutate: KeyedMutator; @@ -99,7 +94,7 @@ const IssueActivitySection: React.FC<{ comment.id, comment ) - .then((response) => { + .then((res) => { mutate(); }); }; @@ -180,6 +175,10 @@ const IssueActivitySection: React.FC<{ ? activity.new_value !== "" ? "marked this issue being blocked by" : "removed blocker" + : activity.field === "target_date" + ? activity.new_value && activity.new_value !== "" + ? "set the due date to" + : "removed the due date" : activityDetails[activity.field as keyof typeof activityDetails] ?.message}{" "} @@ -203,7 +202,9 @@ const IssueActivitySection: React.FC<{ ) : activity.field === "assignee" ? ( activity.old_value ) : activity.field === "target_date" ? ( - renderShortNumericDateFormat(activity.new_value as string) + activity.new_value ? ( + renderShortNumericDateFormat(activity.new_value as string) + ) : null ) : activity.field === "description" ? ( "" ) : ( diff --git a/apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx b/apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx index 5958068b5..46dc55594 100644 --- a/apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx +++ b/apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx @@ -11,7 +11,7 @@ import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/out // services import issuesServices from "services/issues.service"; // types -import { IIssue, IssueResponse } from "types"; +import { IIssue } from "types"; // constants import { PROJECT_ISSUES_LIST, SUB_ISSUES } from "constants/fetch-keys"; @@ -48,16 +48,16 @@ const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parent }) => { }; const addAsSubIssue = (issueId: string) => { - if (workspaceSlug && projectId) { - issuesServices - .patchIssue(workspaceSlug as string, projectId as string, issueId, { parent: parent?.id }) - .then((res) => { - mutate(SUB_ISSUES(parent?.id ?? "")); - }) - .catch((e) => { - console.log(e); - }); - } + if (!workspaceSlug || !projectId) return; + + issuesServices + .patchIssue(workspaceSlug as string, projectId as string, issueId, { parent: parent?.id }) + .then((res) => { + mutate(SUB_ISSUES(parent?.id ?? "")); + }) + .catch((e) => { + console.log(e); + }); }; return ( @@ -140,6 +140,9 @@ const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parent }) => { backgroundColor: issue.state_detail.color, }} /> + + {issue.project_detail.identifier}-{issue.sequence_id} + {issue.name} ); diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx index a59884df2..b46b4c3e1 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx @@ -4,20 +4,12 @@ import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; +// react-hook-form import { useForm, Controller, UseFormWatch, Control } from "react-hook-form"; - +// react-color import { TwitterPicker } from "react-color"; -// services +// headless ui import { Popover, Listbox, Transition } from "@headlessui/react"; -import { - TagIcon, - ChevronDownIcon, - LinkIcon, - CalendarDaysIcon, - TrashIcon, - PlusIcon, - XMarkIcon, -} from "@heroicons/react/24/outline"; // hooks import useToast from "hooks/use-toast"; // services @@ -31,10 +23,19 @@ import SelectCycle from "components/project/issues/issue-detail/issue-detail-sid import SelectAssignee from "components/project/issues/issue-detail/issue-detail-sidebar/select-assignee"; import SelectBlocker from "components/project/issues/issue-detail/issue-detail-sidebar/select-blocker"; import SelectBlocked from "components/project/issues/issue-detail/issue-detail-sidebar/select-blocked"; -// headless ui // ui -import { Input, Button, Spinner } from "components/ui"; +import { Input, Button, Spinner, CustomDatePicker } from "components/ui"; +import DatePicker from "react-datepicker"; // icons +import { + TagIcon, + ChevronDownIcon, + LinkIcon, + CalendarDaysIcon, + TrashIcon, + PlusIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -42,6 +43,8 @@ import type { ICycle, IIssue, IIssueLabels } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS, PROJECT_ISSUES_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; +import "react-datepicker/dist/react-datepicker.css"; + type Props = { control: Control; submitChanges: (formData: Partial) => void; @@ -216,19 +219,37 @@ const IssueDetailSidebar: React.FC = ({

Due date

- ( - { - submitChanges({ target_date: e.target.value }); - onChange(e.target.value); + { + submitChanges({ + target_date: `${val.getFullYear()}-${ + val.getMonth() + 1 + }-${val.getDate()}`, + }); + onChange(`${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}`); + }} + dateFormat="dd-MM-yyyy" + /> + )} + /> */} + ( + { + submitChanges({ + target_date: val + ? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}` + : null, + }); }} - className="w-full cursor-pointer rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" /> )} /> diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx index 02a3fa299..1c5822e7e 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx @@ -259,10 +259,7 @@ const SelectBlocked: React.FC = ({ submitChanges, issuesList, watch }) =>

No issues found. Create a new issue with{" "} -
-                                Ctrl/Command + I
-                              
- . +
C
.

)} diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx index 9df9e8266..e2cb85e6f 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx @@ -258,10 +258,7 @@ const SelectBlocker: React.FC = ({ submitChanges, issuesList, watch }) =>

No issues found. Create a new issue with{" "} -
-                              Ctrl/Command + I
-                            
- . +
C
.

)} diff --git a/apps/app/components/project/issues/issues-list-modal.tsx b/apps/app/components/project/issues/issues-list-modal.tsx index c453b1fea..9c56d6406 100644 --- a/apps/app/components/project/issues/issues-list-modal.tsx +++ b/apps/app/components/project/issues/issues-list-modal.tsx @@ -212,10 +212,7 @@ const IssuesListModal: React.FC = ({

No issues found. Create a new issue with{" "} -
-                              Ctrl/Command + I
-                            
- . +
C
.

)} diff --git a/apps/app/components/project/issues/list-view/index.tsx b/apps/app/components/project/issues/list-view/index.tsx index 0db3a1110..102295f84 100644 --- a/apps/app/components/project/issues/list-view/index.tsx +++ b/apps/app/components/project/issues/list-view/index.tsx @@ -20,7 +20,7 @@ import SingleListIssue from "components/common/list-view/single-issue"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IWorkspaceMember } from "types"; +import { IIssue, IWorkspaceMember, UserAuth } from "types"; // fetch-keys import { STATE_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; @@ -28,10 +28,10 @@ import { STATE_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; type Props = { issues: IIssue[]; handleEditIssue: (issue: IIssue) => void; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + userAuth: UserAuth; }; -const ListView: React.FC = ({ issues, handleEditIssue }) => { +const ListView: React.FC = ({ issues, handleEditIssue, userAuth }) => { const [isCreateIssuesModalOpen, setIsCreateIssuesModalOpen] = useState(false); const [preloadedData, setPreloadedData] = useState< (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined @@ -130,6 +130,7 @@ const ListView: React.FC = ({ issues, handleEditIssue }) => { issue={issue} properties={properties} editIssue={() => handleEditIssue(issue)} + userAuth={userAuth} /> ); }) diff --git a/apps/app/components/project/modules/board-view/index.tsx b/apps/app/components/project/modules/board-view/index.tsx index 0fc8ae2ec..4951f80af 100644 --- a/apps/app/components/project/modules/board-view/index.tsx +++ b/apps/app/components/project/modules/board-view/index.tsx @@ -17,7 +17,7 @@ import SingleBoard from "components/project/modules/board-view/single-board"; // ui import { Spinner } from "components/ui"; // types -import { IIssue, IProjectMember, ModuleIssueResponse } from "types"; +import { IIssue, IProjectMember, ModuleIssueResponse, UserAuth } from "types"; // constants import { STATE_LIST, MODULE_ISSUES } from "constants/fetch-keys"; @@ -26,8 +26,6 @@ type Props = { members: IProjectMember[] | undefined; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; openIssuesListModal: () => void; - removeIssueFromModule: (issueId: string) => void; - partialUpdateIssue: (formData: Partial, issueId: string) => void; handleDeleteIssue: React.Dispatch>; setPreloadedData: React.Dispatch< React.SetStateAction< @@ -37,6 +35,7 @@ type Props = { | null > >; + userAuth: UserAuth; }; const ModulesBoardView: React.FC = ({ @@ -44,10 +43,9 @@ const ModulesBoardView: React.FC = ({ members, openCreateIssueModal, openIssuesListModal, - removeIssueFromModule, - partialUpdateIssue, handleDeleteIssue, setPreloadedData, + userAuth, }) => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; @@ -131,7 +129,7 @@ const ModulesBoardView: React.FC = ({ return ( <> {groupedByIssues ? ( -
+
@@ -154,10 +152,8 @@ const ModulesBoardView: React.FC = ({ : "#000000" } properties={properties} - removeIssueFromModule={removeIssueFromModule} openIssuesListModal={openIssuesListModal} openCreateIssueModal={openCreateIssueModal} - partialUpdateIssue={partialUpdateIssue} handleDeleteIssue={handleDeleteIssue} setPreloadedData={setPreloadedData} stateId={ @@ -165,6 +161,7 @@ const ModulesBoardView: React.FC = ({ ? states?.find((s) => s.name === singleGroup)?.id ?? null : null } + userAuth={userAuth} /> ))}
diff --git a/apps/app/components/project/modules/board-view/single-board.tsx b/apps/app/components/project/modules/board-view/single-board.tsx index 0ddefee68..f6f1cebb8 100644 --- a/apps/app/components/project/modules/board-view/single-board.tsx +++ b/apps/app/components/project/modules/board-view/single-board.tsx @@ -17,7 +17,7 @@ import { CustomMenu } from "components/ui"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; // types -import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types"; +import { IIssue, IWorkspaceMember, NestedKeyOf, Properties, UserAuth } from "types"; // fetch-keys import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; @@ -32,8 +32,6 @@ type Props = { bgColor?: string; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; openIssuesListModal: () => void; - removeIssueFromModule: (bridgeId: string) => void; - partialUpdateIssue: (formData: Partial, issueId: string) => void; handleDeleteIssue: React.Dispatch>; setPreloadedData: React.Dispatch< React.SetStateAction< @@ -44,6 +42,7 @@ type Props = { > >; stateId: string | null; + userAuth: UserAuth; }; const SingleModuleBoard: React.FC = ({ @@ -55,17 +54,16 @@ const SingleModuleBoard: React.FC = ({ bgColor, openCreateIssueModal, openIssuesListModal, - removeIssueFromModule, - partialUpdateIssue, handleDeleteIssue, setPreloadedData, stateId, + userAuth, }) => { // Collapse/Expand const [isCollapsed, setIsCollapsed] = useState(true); const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, moduleId } = router.query; if (selectedGroup === "priority") groupTitle === "high" @@ -112,10 +110,10 @@ const SingleModuleBoard: React.FC = ({ {...provided.droppableProps} ref={provided.innerRef} > - {groupedByIssues[groupTitle].map((childIssue, index: number) => { + {groupedByIssues[groupTitle].map((issue, index: number) => { const assignees = [ - ...(childIssue?.assignees_list ?? []), - ...(childIssue?.assignees ?? []), + ...(issue?.assignees_list ?? []), + ...(issue?.assignees ?? []), ]?.map((assignee) => { const tempPerson = people?.find((p) => p.member.id === assignee)?.member; @@ -123,7 +121,7 @@ const SingleModuleBoard: React.FC = ({ }); return ( - + {(provided, snapshot) => (
= ({ {...provided.dragHandleProps} >
)} diff --git a/apps/app/components/project/modules/confirm-module-deletion.tsx b/apps/app/components/project/modules/confirm-module-deletion.tsx index 2a83633de..df3f32fea 100644 --- a/apps/app/components/project/modules/confirm-module-deletion.tsx +++ b/apps/app/components/project/modules/confirm-module-deletion.tsx @@ -1,19 +1,21 @@ -// react import React, { useEffect, useRef, useState } from "react"; -// next + import { useRouter } from "next/router"; -// swr + import { mutate } from "swr"; -// services + // headless ui import { Dialog, Transition } from "@headlessui/react"; +// services +import modulesService from "services/modules.service"; +// hooks +import useToast from "hooks/use-toast"; // ui +import { Button } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types import type { IModule } from "types"; -import { Button } from "components/ui"; -import modulesService from "services/modules.service"; // fetch-keys import { MODULE_LIST } from "constants/fetch-keys"; @@ -31,6 +33,8 @@ const ConfirmModuleDeletion: React.FC = ({ isOpen, setIsOpen, data }) => query: { workspaceSlug }, } = router; + const { setToastAlert } = useToast(); + const cancelButtonRef = useRef(null); const handleClose = () => { @@ -48,6 +52,12 @@ const ConfirmModuleDeletion: React.FC = ({ isOpen, setIsOpen, data }) => mutate(MODULE_LIST(data.project)); router.push(`/${workspaceSlug}/projects/${data.project}/modules`); handleClose(); + + setToastAlert({ + title: "Success", + type: "success", + message: "Module deleted successfully", + }); }) .catch((error) => { console.log(error); diff --git a/apps/app/components/project/modules/create-update-module-modal/index.tsx b/apps/app/components/project/modules/create-update-module-modal/index.tsx index e5e3668f1..b0ed3d561 100644 --- a/apps/app/components/project/modules/create-update-module-modal/index.tsx +++ b/apps/app/components/project/modules/create-update-module-modal/index.tsx @@ -4,22 +4,25 @@ import { useRouter } from "next/router"; import { mutate } from "swr"; -import { useForm } from "react-hook-form"; - +// react-hook-form +import { Controller, useForm } from "react-hook-form"; +// headless ui import { Dialog, Transition } from "@headlessui/react"; -// types -import type { IModule } from "types"; // components import SelectLead from "components/project/modules/create-update-module-modal/select-lead"; import SelectMembers from "components/project/modules/create-update-module-modal/select-members"; import SelectStatus from "components/project/modules/create-update-module-modal/select-status"; // ui -import { Button, Input, TextArea } from "components/ui"; +import { Button, CustomDatePicker, Input, TextArea } from "components/ui"; // services import modulesService from "services/modules.service"; +// hooks +import useToast from "hooks/use-toast"; // helpers import { renderDateFormat } from "helpers/date-time.helper"; -// fetch keys +// types +import type { IModule } from "types"; +// fetch-keys import { MODULE_LIST } from "constants/fetch-keys"; type Props = { @@ -41,6 +44,8 @@ const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, pro const router = useRouter(); const { workspaceSlug } = router.query; + const { setToastAlert } = useToast(); + const { register, formState: { errors, isSubmitting }, @@ -65,6 +70,12 @@ const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, pro .then(() => { mutate(MODULE_LIST(projectId)); handleClose(); + + setToastAlert({ + title: "Success", + type: "success", + message: "Module created successfully", + }); }) .catch((err) => { Object.keys(err).map((key) => { @@ -91,6 +102,12 @@ const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, pro false ); handleClose(); + + setToastAlert({ + title: "Success", + type: "success", + message: "Module updated successfully", + }); }) .catch((err) => { Object.keys(err).map((key) => { @@ -161,6 +178,10 @@ const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, pro register={register} validations={{ required: "Name is required", + maxLength: { + value: 255, + message: "Name should be less than 255 characters", + }, }} />
@@ -176,32 +197,62 @@ const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, pro
- +
Start Date
+
+ ( + { + onChange( + val + ? `${val.getFullYear()}-${ + val.getMonth() + 1 + }-${val.getDate()}` + : null + ); + }} + error={errors.start_date ? true : false} + /> + )} + /> + {errors.start_date && ( +
{errors.start_date.message}
+ )} +
- +
Target Date
+
+ ( + { + onChange( + val + ? `${val.getFullYear()}-${ + val.getMonth() + 1 + }-${val.getDate()}` + : null + ); + }} + error={errors.target_date ? true : false} + /> + )} + /> + {errors.target_date && ( +
{errors.target_date.message}
+ )} +
diff --git a/apps/app/components/project/modules/list-view/index.tsx b/apps/app/components/project/modules/list-view/index.tsx index 1148bb0ca..5abbcc31c 100644 --- a/apps/app/components/project/modules/list-view/index.tsx +++ b/apps/app/components/project/modules/list-view/index.tsx @@ -18,7 +18,7 @@ import { CustomMenu, Spinner } from "components/ui"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IWorkspaceMember } from "types"; +import { IIssue, IWorkspaceMember, UserAuth } from "types"; // fetch-keys import { STATE_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; @@ -35,6 +35,7 @@ type Props = { | null > >; + userAuth: UserAuth; }; const ModulesListView: React.FC = ({ @@ -43,6 +44,7 @@ const ModulesListView: React.FC = ({ openIssuesListModal, removeIssueFromModule, setPreloadedData, + userAuth, }) => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; @@ -137,6 +139,7 @@ const ModulesListView: React.FC = ({ properties={properties} editIssue={() => openCreateIssueModal(issue, "edit")} removeIssue={() => removeIssueFromModule(issue.bridge ?? "")} + userAuth={userAuth} /> ); }) diff --git a/apps/app/components/project/modules/module-detail-sidebar/index.tsx b/apps/app/components/project/modules/module-detail-sidebar/index.tsx index 0c29f9936..dce925d1d 100644 --- a/apps/app/components/project/modules/module-detail-sidebar/index.tsx +++ b/apps/app/components/project/modules/module-detail-sidebar/index.tsx @@ -22,8 +22,11 @@ import SelectLead from "components/project/modules/module-detail-sidebar/select- import SelectMembers from "components/project/modules/module-detail-sidebar/select-members"; import SelectStatus from "components/project/modules/module-detail-sidebar/select-status"; import ModuleLinkModal from "components/project/modules/module-link-modal"; +//progress-bar +import { CircularProgressbar } from "react-circular-progressbar"; +import "react-circular-progressbar/dist/styles.css"; // ui -import { Loader } from "components/ui"; +import { CustomDatePicker, Loader } from "components/ui"; // icons // helpers import { timeAgo } from "helpers/date-time.helper"; @@ -36,8 +39,8 @@ import { MODULE_LIST } from "constants/fetch-keys"; const defaultValues: Partial = { members_list: [], - start_date: new Date().toString(), - target_date: new Date().toString(), + start_date: null, + target_date: null, status: null, }; @@ -85,16 +88,21 @@ const ModuleDetailSidebar: React.FC = ({ const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !module) return; + mutate( + projectId && MODULE_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((module) => { + if (module.id === moduleId) return { ...module, ...data }; + return module; + }), + false + ); + modulesService .patchModule(workspaceSlug as string, projectId as string, module.id, data) .then((res) => { console.log(res); - mutate(projectId && MODULE_LIST(projectId as string), (prevData) => - (prevData ?? []).map((module) => { - if (module.id === moduleId) return { ...module, ...data }; - return module; - }) - ); + mutate(MODULE_LIST(projectId as string)); }) .catch((e) => { console.log(e); @@ -161,7 +169,13 @@ const ModuleDetailSidebar: React.FC = ({
- + + +
{groupedIssues.completed.length}/{moduleIssues?.length}
@@ -177,16 +191,16 @@ const ModuleDetailSidebar: React.FC = ({ ( - { - submitChanges({ start_date: e.target.value }); - onChange(e.target.value); + render={({ field: { value } }) => ( + { + submitChanges({ + start_date: val + ? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}` + : null, + }); }} - className="w-full cursor-pointer rounded-md border bg-transparent px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" /> )} /> @@ -201,16 +215,16 @@ const ModuleDetailSidebar: React.FC = ({ ( - { - submitChanges({ target_date: e.target.value }); - onChange(e.target.value); + render={({ field: { value } }) => ( + { + submitChanges({ + target_date: val + ? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}` + : null, + }); }} - className="w-full cursor-pointer rounded-md border bg-transparent px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" /> )} /> diff --git a/apps/app/components/project/modules/module-detail-sidebar/select-members.tsx b/apps/app/components/project/modules/module-detail-sidebar/select-members.tsx index 4a4896fc8..1e862c9de 100644 --- a/apps/app/components/project/modules/module-detail-sidebar/select-members.tsx +++ b/apps/app/components/project/modules/module-detail-sidebar/select-members.tsx @@ -12,9 +12,7 @@ import { UserGroupIcon } from "@heroicons/react/24/outline"; import workspaceService from "services/workspace.service"; // headless ui // ui -import { Spinner } from "components/ui"; -// icons -import User from "public/user.png"; +import { AssigneesList, Spinner } from "components/ui"; // types import { IModule } from "types"; // constants @@ -64,52 +62,7 @@ const SelectMembers: React.FC = ({ control, submitChanges }) => { >
{value && Array.isArray(value) ? ( - <> - {value.length > 0 ? ( - value.map((member, index: number) => { - const person = people?.find((p) => p.member.id === member)?.member; - - return ( -
- {person && person.avatar && person.avatar !== "" ? ( -
- {person.first_name} -
- ) : ( -
- {person?.first_name && person.first_name !== "" - ? person.first_name.charAt(0) - : person?.email.charAt(0)} -
- )} -
- ); - }) - ) : ( -
- No user -
- )} - + ) : null}
diff --git a/apps/app/components/project/modules/single-module-card.tsx b/apps/app/components/project/modules/single-module-card.tsx index ec151f242..acd6d4d98 100644 --- a/apps/app/components/project/modules/single-module-card.tsx +++ b/apps/app/components/project/modules/single-module-card.tsx @@ -1,14 +1,15 @@ -import React from "react"; +import React, { useState } from "react"; import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; +import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline"; +import ConfirmModuleDeletion from "./confirm-module-deletion"; // icons -import { CalendarDaysIcon } from "@heroicons/react/24/outline"; import User from "public/user.png"; // helpers import { renderShortNumericDateFormat } from "helpers/date-time.helper"; // types -import { IModule } from "types"; +import { IModule, SelectModuleType } from "types"; // common import { MODULE_STATUS } from "constants/"; @@ -19,103 +20,131 @@ type Props = { const SingleModuleCard: React.FC = ({ module }) => { const router = useRouter(); const { workspaceSlug } = router.query; + const [moduleDeleteModal, setModuleDeleteModal] = useState(false); + const [selectedModuleForDelete, setSelectedModuleForDelete] = useState(); + const handleDeleteModule = () => { + if (!module) return; + + setSelectedModuleForDelete({ ...module, actionType: "delete" }); + setModuleDeleteModal(true); + }; return ( - - - {module.name} -
-
-
LEAD
-
- {module.lead ? ( - module.lead_detail?.avatar && module.lead_detail.avatar !== "" ? ( -
+
+
+ +
+ + +
+ {module.name} +
+
+
LEAD
+
+ {module.lead ? ( + module.lead_detail?.avatar && module.lead_detail.avatar !== "" ? ( +
+ {module.lead_detail.first_name} +
+ ) : ( +
+ {module.lead_detail?.first_name && module.lead_detail.first_name !== "" + ? module.lead_detail.first_name.charAt(0) + : module.lead_detail?.email.charAt(0)} +
+ ) + ) : ( + "N/A" + )} +
+
+
+
MEMBERS
+
+ {module.members && module.members.length > 0 ? ( + module?.members_detail?.map((member, index: number) => ( +
+ {member?.avatar && member.avatar !== "" ? ( +
+ {member?.first_name} +
+ ) : ( +
+ {member?.first_name && member.first_name !== "" + ? member.first_name.charAt(0) + : member?.email?.charAt(0)} +
+ )} +
+ )) + ) : ( +
{module.lead_detail.first_name}
- ) : ( -
- {module.lead_detail?.first_name && module.lead_detail.first_name !== "" - ? module.lead_detail.first_name.charAt(0) - : module.lead_detail?.email.charAt(0)} -
- ) - ) : ( - "N/A" - )} + )} +
+
+
+
END DATE
+
+ + {renderShortNumericDateFormat(module.target_date ?? "")} +
+
+
+
STATUS
+
+ s.value === module.status)?.color, + }} + /> + {module.status} +
-
-
MEMBERS
-
- {module.members && module.members.length > 0 ? ( - module?.members_detail?.map((member, index: number) => ( -
- {member?.avatar && member.avatar !== "" ? ( -
- {member?.first_name} -
- ) : ( -
- {member?.first_name && member.first_name !== "" - ? member.first_name.charAt(0) - : member?.email?.charAt(0)} -
- )} -
- )) - ) : ( -
- No user -
- )} -
-
-
-
END DATE
-
- - {renderShortNumericDateFormat(module.target_date ?? "")} -
-
-
-
STATUS
-
- s.value === module.status)?.color, - }} - /> - {module.status} -
-
-
- - + + +
); }; diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx index 2d6e38355..a7908421a 100644 --- a/apps/app/components/project/send-project-invitation-modal.tsx +++ b/apps/app/components/project/send-project-invitation-modal.tsx @@ -9,7 +9,7 @@ import { useForm, Controller } from "react-hook-form"; import { Dialog, Transition, Listbox } from "@headlessui/react"; // ui import { ChevronDownIcon, CheckIcon } from "@heroicons/react/20/solid"; -import { Button, Select, TextArea } from "components/ui"; +import { Button, CustomSelect, Select, TextArea } from "components/ui"; // hooks import useToast from "hooks/use-toast"; // services @@ -106,7 +106,7 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member return ( - + = ({ isOpen, setIsOpen, member
-
+
= ({ isOpen, setIsOpen, member uninvitedPeople?.map((person) => ( - `${ - active ? "bg-theme text-white" : "text-gray-900" - } relative cursor-default select-none py-2 pl-3 pr-9 text-left` + className={({ active, selected }) => + `${active ? "bg-indigo-50" : ""} ${ + selected ? "bg-indigo-50 font-medium" : "" + } text-gray-900 cursor-default select-none p-2` } value={{ id: person.member.id, email: person.member.email, }} > - {({ selected, active }) => ( - <> - - {person.member.email} - - - {selected ? ( - - - ) : null} - - )} + {person.member.email} )) )} @@ -246,22 +223,28 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member />
-
-