diff --git a/.env.example b/.env.example index 118a94883..42d98677b 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,68 @@ -# Replace with your instance Public IP +# Frontend +# Extra image domains that need to be added for Next Image NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= +# Google Client ID for Google OAuth NEXT_PUBLIC_GOOGLE_CLIENTID="" -NEXT_PUBLIC_GITHUB_APP_NAME="" +# Github ID for Github OAuth NEXT_PUBLIC_GITHUB_ID="" +# Github App Name for GitHub Integration +NEXT_PUBLIC_GITHUB_APP_NAME="" +# Sentry DSN for error monitoring NEXT_PUBLIC_SENTRY_DSN="" +# Enable/Disable OAUTH - default 0 for selfhosted instance NEXT_PUBLIC_ENABLE_OAUTH=0 +# Enable/Disable sentry NEXT_PUBLIC_ENABLE_SENTRY=0 +# Enable/Disable session recording NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0 +# Enable/Disable event tracking NEXT_PUBLIC_TRACK_EVENTS=0 +# Slack for Slack Integration NEXT_PUBLIC_SLACK_CLIENT_ID="" + +# Backend + +# Database Settings +PGUSER="plane" +PGPASSWORD="plane" +PGHOST="plane-db" +PGDATABASE="plane" + +# Email Settings EMAIL_HOST="" EMAIL_HOST_USER="" EMAIL_HOST_PASSWORD="" +EMAIL_PORT=587 +EMAIL_FROM="Team Plane " +EMAIL_USE_TLS="1" + +# AWS Settings AWS_REGION="" -AWS_ACCESS_KEY_ID="" -AWS_SECRET_ACCESS_KEY="" -AWS_S3_BUCKET_NAME="" +AWS_ACCESS_KEY_ID="access-key" +AWS_SECRET_ACCESS_KEY="secret-key" +AWS_S3_ENDPOINT_URL="http://plane-minio:9000" +# Changing this requires change in the nginx.conf for uploads if using minio setup +AWS_S3_BUCKET_NAME="uploads" +# Maximum file upload limit +FILE_SIZE_LIMIT=5242880 + +# GPT settings OPENAI_API_KEY="" -GPT_ENGINE="" \ No newline at end of file +GPT_ENGINE="" + +# Github +GITHUB_CLIENT_SECRET="" # For fetching release notes + +# Settings related to Docker +DOCKERIZED=1 +# set to 1 If using the pre-configured minio setup +USE_MINIO=1 + +# Nginx Configuration +NGINX_PORT=80 + +# Default Creds +DEFAULT_EMAIL="captain@plane.so" +DEFAULT_PASSWORD="password123" + +# Auto generated and Required that will be generated from setup.sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cb7ef6887..5cff15dc5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ FROM node:18-alpine AS builder RUN apk add --no-cache libc6-compat -RUN apk update # Set working directory WORKDIR /app ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER @@ -13,9 +12,7 @@ RUN turbo prune --scope=app --docker # Add lockfile and package.json's of isolated subworkspace FROM node:18-alpine AS installer - RUN apk add --no-cache libc6-compat -RUN apk update WORKDIR /app ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 # First install the dependencies (as they change less often) @@ -44,10 +41,12 @@ FROM python:3.11.1-alpine3.17 AS backend ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 ENV PIP_DISABLE_PIP_VERSION_CHECK=1 +ENV DJANGO_SETTINGS_MODULE plane.settings.production +ENV DOCKERIZED 1 WORKDIR /code -RUN apk --update --no-cache add \ +RUN apk --no-cache add \ "libpq~=15" \ "libxslt~=1.1" \ "nodejs-current~=19" \ @@ -59,8 +58,8 @@ RUN apk --update --no-cache add \ COPY apiserver/requirements.txt ./ COPY apiserver/requirements ./requirements -RUN apk add libffi-dev -RUN apk --update --no-cache --virtual .build-deps add \ +RUN apk add --no-cache libffi-dev +RUN apk add --no-cache --virtual .build-deps \ "bash~=5.2" \ "g++~=12.2" \ "gcc~=12.2" \ @@ -81,18 +80,13 @@ COPY apiserver/plane plane/ COPY apiserver/templates templates/ COPY apiserver/gunicorn.config.py ./ -RUN apk --update --no-cache add "bash~=5.2" +RUN apk --no-cache add "bash~=5.2" COPY apiserver/bin ./bin/ RUN chmod +x ./bin/takeoff ./bin/worker RUN chmod -R 777 /code # Expose container port and run entry point script -EXPOSE 8000 -EXPOSE 3000 -EXPOSE 80 - - WORKDIR /app @@ -126,9 +120,6 @@ COPY start.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/replace-env-vars.sh RUN chmod +x /usr/local/bin/start.sh +EXPOSE 80 CMD ["supervisord","-c","/code/supervisor.conf"] - - - - diff --git a/README.md b/README.md index 827a2b146..cf0af7fe2 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,18 @@ Discord

-
+

- + Plane Screens + + + Plane Screens @@ -38,22 +45,18 @@ The easiest way to get started with Plane is by creating a [Plane Cloud](https:/ ### Docker Compose Setup -- Clone the Repository +- Clone the repository ```bash git clone https://github.com/makeplane/plane -``` - -- Change Directory - -```bash cd plane +chmod +x setup.sh ``` - Run setup.sh ```bash -./setup.sh localhost +./setup.sh http://localhost ``` > If running in a cloud env replace localhost with public facing IP address of the VM @@ -69,7 +72,7 @@ set +a - Run Docker compose up ```bash -docker-compose -f docker-compose-hub.yml up +docker compose up -d ``` You can use the default email and password for your first login `captain@plane.so` and `password123`. @@ -89,41 +92,62 @@ docker-compose -f docker-compose-hub.yml up ## 📸 Screenshots

- + Plane Views + +

+

+ + Plane Issue Details -

-

- +

+

+ Plane Cycles and Modules -

-

- +

+

+ Plane Quick Lists -

-

- +

+

+ Plane Command K +

+

+ + Plane Command Menu + +

+

+ ## 📚Documentation diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 123544571..402940f83 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -7,7 +7,7 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 WORKDIR /code -RUN apk --update --no-cache add \ +RUN apk --no-cache add \ "libpq~=15" \ "libxslt~=1.1" \ "nodejs-current~=19" \ @@ -15,8 +15,8 @@ RUN apk --update --no-cache add \ COPY requirements.txt ./ COPY requirements ./requirements -RUN apk add libffi-dev -RUN apk --update --no-cache --virtual .build-deps add \ +RUN apk add --no-cache libffi-dev +RUN apk add --no-cache --virtual .build-deps \ "bash~=5.2" \ "g++~=12.2" \ "gcc~=12.2" \ @@ -46,7 +46,7 @@ COPY templates templates/ COPY gunicorn.config.py ./ USER root -RUN apk --update --no-cache add "bash~=5.2" +RUN apk --no-cache add "bash~=5.2" COPY ./bin ./bin/ RUN chmod +x ./bin/takeoff ./bin/worker diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index a39128088..d3c17d057 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -1,3 +1,6 @@ +# Django imports +from django.utils import timezone + # Third Party imports from rest_framework import serializers @@ -251,6 +254,7 @@ class IssueCreateSerializer(BaseSerializer): batch_size=10, ) + instance.updated_at = timezone.now() return super().update(instance, validated_data) diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index 14a33d9c3..d8978479e 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -25,6 +25,10 @@ class UserSerializer(BaseSerializer): ] extra_kwargs = {"password": {"write_only": True}} + # If the user has already filled first name or last name then he is onboarded + def get_is_onboarded(self, obj): + return bool(obj.first_name) or bool(obj.last_name) + class UserLiteSerializer(BaseSerializer): class Meta: diff --git a/apiserver/plane/api/serializers/workspace.py b/apiserver/plane/api/serializers/workspace.py index 4f4d13f76..078a4bf08 100644 --- a/apiserver/plane/api/serializers/workspace.py +++ b/apiserver/plane/api/serializers/workspace.py @@ -44,6 +44,8 @@ class WorkSpaceMemberSerializer(BaseSerializer): class WorkSpaceMemberInviteSerializer(BaseSerializer): workspace = WorkSpaceSerializer(read_only=True) + total_members = serializers.IntegerField(read_only=True) + created_by_detail = UserLiteSerializer(read_only=True, source="created_by") class Meta: model = WorkspaceMemberInvite diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 93af9d762..bf5180ff8 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -96,12 +96,8 @@ from plane.api.views import ( CycleViewSet, CycleIssueViewSet, CycleDateCheckEndpoint, - CurrentUpcomingCyclesEndpoint, - CompletedCyclesEndpoint, CycleFavoriteViewSet, - DraftCyclesEndpoint, TransferCycleIssueEndpoint, - InCompleteCyclesEndpoint, ## End Cycles # Modules ModuleViewSet, @@ -115,10 +111,6 @@ from plane.api.views import ( PageBlockViewSet, PageFavoriteViewSet, CreateIssueFromPageBlockEndpoint, - RecentPagesEndpoint, - FavoritePagesEndpoint, - MyPagesEndpoint, - CreatedbyOtherPagesEndpoint, ## End Pages # Api Tokens ApiTokenEndpoint, @@ -178,7 +170,7 @@ urlpatterns = [ ), # Password Manipulation path( - "password-reset///", + "reset-password///", ResetPasswordEndpoint.as_view(), name="password-reset", ), @@ -664,21 +656,6 @@ urlpatterns = [ CycleDateCheckEndpoint.as_view(), name="project-cycle", ), - path( - "workspaces//projects//cycles/current-upcoming-cycles/", - CurrentUpcomingCyclesEndpoint.as_view(), - name="project-cycle-upcoming", - ), - path( - "workspaces//projects//cycles/completed-cycles/", - CompletedCyclesEndpoint.as_view(), - name="project-cycle-completed", - ), - path( - "workspaces//projects//cycles/draft-cycles/", - DraftCyclesEndpoint.as_view(), - name="project-cycle-draft", - ), path( "workspaces//projects//user-favorite-cycles/", CycleFavoriteViewSet.as_view( @@ -703,11 +680,6 @@ urlpatterns = [ TransferCycleIssueEndpoint.as_view(), name="transfer-issues", ), - path( - "workspaces//projects//incomplete-cycles/", - InCompleteCyclesEndpoint.as_view(), - name="transfer-issues", - ), ## End Cycles # Issue path( @@ -1077,26 +1049,6 @@ urlpatterns = [ CreateIssueFromPageBlockEndpoint.as_view(), name="page-block-issues", ), - path( - "workspaces//projects//pages/recent-pages/", - RecentPagesEndpoint.as_view(), - name="recent-pages", - ), - path( - "workspaces//projects//pages/favorite-pages/", - FavoritePagesEndpoint.as_view(), - name="recent-pages", - ), - path( - "workspaces//projects//pages/my-pages/", - MyPagesEndpoint.as_view(), - name="user-pages", - ), - path( - "workspaces//projects//pages/created-by-other-pages/", - CreatedbyOtherPagesEndpoint.as_view(), - name="created-by-other-pages", - ), ## End Pages # API Tokens path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 65554f529..4177b1371 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -49,12 +49,8 @@ from .cycle import ( CycleViewSet, CycleIssueViewSet, CycleDateCheckEndpoint, - CurrentUpcomingCyclesEndpoint, - CompletedCyclesEndpoint, CycleFavoriteViewSet, - DraftCyclesEndpoint, TransferCycleIssueEndpoint, - InCompleteCyclesEndpoint, ) from .asset import FileAssetEndpoint, UserAssetsEndpoint from .issue import ( @@ -122,10 +118,6 @@ from .page import ( PageBlockViewSet, PageFavoriteViewSet, CreateIssueFromPageBlockEndpoint, - RecentPagesEndpoint, - FavoritePagesEndpoint, - MyPagesEndpoint, - CreatedbyOtherPagesEndpoint, ) from .search import GlobalSearchEndpoint, IssueSearchEndpoint diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py index 98c9f9caf..0b935a4d3 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/api/views/asset.py @@ -3,10 +3,10 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.parsers import MultiPartParser, FormParser from sentry_sdk import capture_exception - +from django.conf import settings # Module imports from .base import BaseAPIView -from plane.db.models import FileAsset +from plane.db.models import FileAsset, Workspace from plane.api.serializers import FileAssetSerializer @@ -27,15 +27,13 @@ class FileAssetEndpoint(BaseAPIView): try: serializer = FileAssetSerializer(data=request.data) if serializer.is_valid(): - if request.user.last_workspace_id is None: - return Response( - {"error": "Workspace id is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer.save(workspace_id=request.user.last_workspace_id) + # Get the workspace + workspace = Workspace.objects.get(slug=slug) + serializer.save(workspace_id=workspace.id) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Workspace.DoesNotExist: + return Response({"error": "Workspace does not exist"}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: capture_exception(e) return Response( diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index b12b49b2f..63c832e71 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -152,6 +152,75 @@ class CycleViewSet(BaseViewSet): .distinct() ) + def list(self, request, slug, project_id): + try: + queryset = self.get_queryset() + cycle_view = request.GET.get("cycle_view", False) + if not cycle_view: + return Response( + {"error": "Cycle View parameter is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # All Cycles + if cycle_view == "all": + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + # Current Cycle + if cycle_view == "current": + queryset = queryset.filter( + start_date__lte=timezone.now(), + end_date__gte=timezone.now(), + ) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + # Upcoming Cycles + if cycle_view == "upcoming": + queryset = queryset.filter(start_date__gt=timezone.now()) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + # Completed Cycles + if cycle_view == "completed": + queryset = queryset.filter(end_date__lt=timezone.now()) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + # Draft Cycles + if cycle_view == "draft": + queryset = queryset.filter( + end_date=None, + start_date=None, + ) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + # Incomplete Cycles + if cycle_view == "incomplete": + queryset = queryset.filter( + Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True), + ) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + return Response( + {"error": "No matching view found"}, 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, + ) + def create(self, request, slug, project_id): try: if ( @@ -478,352 +547,6 @@ class CycleDateCheckEndpoint(BaseAPIView): ) -class CurrentUpcomingCyclesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = CycleFavorite.objects.filter( - user=self.request.user, - cycle_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - current_cycle = ( - Cycle.objects.filter( - workspace__slug=slug, - project_id=project_id, - start_date__lte=timezone.now(), - end_date__gte=timezone.now(), - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), - ) - ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .prefetch_related( - Prefetch( - "issue_cycle__issue__assignees", - queryset=User.objects.only( - "avatar", "first_name", "id" - ).distinct(), - ) - ) - .order_by("name", "-is_favorite") - ) - - upcoming_cycle = ( - Cycle.objects.filter( - workspace__slug=slug, - project_id=project_id, - start_date__gt=timezone.now(), - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), - ) - ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .prefetch_related( - Prefetch( - "issue_cycle__issue__assignees", - queryset=User.objects.only( - "avatar", "first_name", "id" - ).distinct(), - ) - ) - .order_by("name", "-is_favorite") - ) - - return Response( - { - "current_cycle": CycleSerializer(current_cycle, many=True).data, - "upcoming_cycle": CycleSerializer(upcoming_cycle, many=True).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, - ) - - -class CompletedCyclesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = CycleFavorite.objects.filter( - user=self.request.user, - cycle_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - completed_cycles = ( - Cycle.objects.filter( - workspace__slug=slug, - project_id=project_id, - end_date__lt=timezone.now(), - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), - ) - ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .prefetch_related( - Prefetch( - "issue_cycle__issue__assignees", - queryset=User.objects.only( - "avatar", "first_name", "id" - ).distinct(), - ) - ) - .order_by("name", "-is_favorite") - ) - - return Response( - { - "completed_cycles": CycleSerializer( - completed_cycles, many=True - ).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, - ) - - -class DraftCyclesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = CycleFavorite.objects.filter( - user=self.request.user, - cycle_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - draft_cycles = ( - Cycle.objects.filter( - workspace__slug=slug, - project_id=project_id, - end_date=None, - start_date=None, - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), - ) - ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .prefetch_related( - Prefetch( - "issue_cycle__issue__assignees", - queryset=User.objects.only( - "avatar", "first_name", "id" - ).distinct(), - ) - ) - .order_by("name", "-is_favorite") - ) - - return Response( - {"draft_cycles": CycleSerializer(draft_cycles, many=True).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, - ) - - class CycleFavoriteViewSet(BaseViewSet): permission_classes = [ ProjectEntityPermission, @@ -948,22 +671,3 @@ class TransferCycleIssueEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) - - -class InCompleteCyclesEndpoint(BaseAPIView): - def get(self, request, slug, project_id): - try: - cycles = Cycle.objects.filter( - Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True), - workspace__slug=slug, - project_id=project_id, - ).select_related("owned_by") - - serializer = CycleSerializer(cycles, 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, - ) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 987677bb2..e6c37374b 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -4,11 +4,23 @@ import random from itertools import chain # Django imports -from django.db.models import Prefetch, OuterRef, Func, F, Q, Count +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Count, + Case, + Value, + CharField, + When, +) from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page from django.db.models.functions import Coalesce +from django.conf import settings # Third Party imports from rest_framework.response import Response @@ -144,9 +156,13 @@ class IssueViewSet(BaseViewSet): filters = issue_filters(request.query_params, "GET") show_sub_issues = request.GET.get("show_sub_issues", "true") + # Custom ordering for priority + priority_order = ["urgent", "high", "medium", "low", None] + + order_by_param = request.GET.get("order_by", "-created_at") + issue_queryset = ( self.get_queryset() - .order_by(request.GET.get("order_by", "created_at")) .filter(**filters) .annotate(cycle_id=F("issue_cycle__id")) .annotate(module_id=F("issue_module__id")) @@ -166,6 +182,19 @@ class IssueViewSet(BaseViewSet): ) ) + if order_by_param == "priority": + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + else: + issue_queryset = issue_queryset.order_by(order_by_param) + issue_queryset = ( issue_queryset if show_sub_issues == "true" diff --git a/apiserver/plane/api/views/page.py b/apiserver/plane/api/views/page.py index 88ce318cf..edca47ffe 100644 --- a/apiserver/plane/api/views/page.py +++ b/apiserver/plane/api/views/page.py @@ -125,7 +125,57 @@ class PageViewSet(BaseViewSet): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + def list(self, request, slug, project_id): + try: + queryset = self.get_queryset() + page_view = request.GET.get("page_view", False) + if not page_view: + return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST) + + # All Pages + if page_view == "all": + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + # Recent pages + if page_view == "recent": + current_time = date.today() + day_before = current_time - timedelta(days=1) + todays_pages = queryset.filter(updated_at__date=date.today()) + yesterdays_pages = queryset.filter(updated_at__date=day_before) + earlier_this_week = queryset.filter( updated_at__date__range=( + (timezone.now() - timedelta(days=7)), + (timezone.now() - timedelta(days=2)), + )) + return Response( + { + "today": PageSerializer(todays_pages, many=True).data, + "yesterday": PageSerializer(yesterdays_pages, many=True).data, + "earlier_this_week": PageSerializer(earlier_this_week, many=True).data, + }, + status=status.HTTP_200_OK, + ) + + # Favorite Pages + if page_view == "favorite": + queryset = queryset.filter(is_favorite=True) + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + # My pages + if page_view == "created_by_me": + queryset = queryset.filter(owned_by=request.user) + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + # Created by other Pages + if page_view == "created_by_other": + queryset = queryset.filter(~Q(owned_by=request.user), access=0) + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + return Response({"error": "No matching view found"}, 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) class PageBlockViewSet(BaseViewSet): serializer_class = PageBlockSerializer @@ -269,249 +319,3 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) - - -class RecentPagesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = PageFavorite.objects.filter( - user=request.user, - page_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - current_time = date.today() - day_before = current_time - timedelta(days=1) - - todays_pages = ( - Page.objects.filter( - updated_at__date=date.today(), - workspace__slug=slug, - project_id=project_id, - ) - .filter(project__project_projectmember__member=request.user) - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "-updated_at") - ) - - yesterdays_pages = ( - Page.objects.filter( - updated_at__date=day_before, - workspace__slug=slug, - project_id=project_id, - ) - .filter(project__project_projectmember__member=request.user) - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "-updated_at") - ) - - earlier_this_week = ( - Page.objects.filter( - updated_at__date__range=( - (timezone.now() - timedelta(days=7)), - (timezone.now() - timedelta(days=2)), - ), - workspace__slug=slug, - project_id=project_id, - ) - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .filter(project__project_projectmember__member=request.user) - .annotate(is_favorite=Exists(subquery)) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "-updated_at") - ) - todays_pages_serializer = PageSerializer(todays_pages, many=True) - yesterday_pages_serializer = PageSerializer(yesterdays_pages, many=True) - earlier_this_week_serializer = PageSerializer(earlier_this_week, many=True) - return Response( - { - "today": todays_pages_serializer.data, - "yesterday": yesterday_pages_serializer.data, - "earlier_this_week": earlier_this_week_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, - ) - - -class FavoritePagesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = PageFavorite.objects.filter( - user=request.user, - page_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - pages = ( - Page.objects.filter( - workspace__slug=slug, - project_id=project_id, - ) - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .filter(project__project_projectmember__member=request.user) - .filter(is_favorite=True) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("name", "-is_favorite") - ) - - serializer = PageSerializer(pages, 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, - ) - - -class MyPagesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = PageFavorite.objects.filter( - user=request.user, - page_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - pages = ( - Page.objects.filter( - workspace__slug=slug, project_id=project_id, owned_by=request.user - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .filter(project__project_projectmember__member=request.user) - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "name") - ) - serializer = PageSerializer(pages, 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, - ) - - -class CreatedbyOtherPagesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = PageFavorite.objects.filter( - user=request.user, - page_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - pages = ( - Page.objects.filter( - ~Q(owned_by=request.user), - workspace__slug=slug, - project_id=project_id, - access=0, - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .annotate(is_favorite=Exists(subquery)) - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "name") - ) - serializer = PageSerializer(pages, 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, - ) diff --git a/apiserver/plane/api/views/people.py b/apiserver/plane/api/views/people.py index 78ae5b2fc..fcf95ff64 100644 --- a/apiserver/plane/api/views/people.py +++ b/apiserver/plane/api/views/people.py @@ -31,36 +31,61 @@ class UserEndpoint(BaseViewSet): def retrieve(self, request): try: - workspace = Workspace.objects.get(pk=request.user.last_workspace_id) + workspace = Workspace.objects.get( + pk=request.user.last_workspace_id, workspace_member__member=request.user + ) workspace_invites = WorkspaceMemberInvite.objects.filter( email=request.user.email ).count() assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count() + serialized_data = UserSerializer(request.user).data + serialized_data["workspace"] = { + "last_workspace_id": request.user.last_workspace_id, + "last_workspace_slug": workspace.slug, + "fallback_workspace_id": request.user.last_workspace_id, + "fallback_workspace_slug": workspace.slug, + "invites": workspace_invites, + } + serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues + return Response( - { - "user": UserSerializer(request.user).data, - "slug": workspace.slug, - "workspace_invites": workspace_invites, - "assigned_issues": assigned_issues, - }, + serialized_data, status=status.HTTP_200_OK, ) except Workspace.DoesNotExist: + # This exception will be hit even when the `last_workspace_id` is None + workspace_invites = WorkspaceMemberInvite.objects.filter( email=request.user.email ).count() assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count() + + fallback_workspace = Workspace.objects.filter( + workspace_member__member=request.user + ).order_by("created_at").first() + + serialized_data = UserSerializer(request.user).data + + serialized_data["workspace"] = { + "last_workspace_id": None, + "last_workspace_slug": None, + "fallback_workspace_id": fallback_workspace.id + if fallback_workspace is not None + else None, + "fallback_workspace_slug": fallback_workspace.slug + if fallback_workspace is not None + else None, + "invites": workspace_invites, + } + serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues + return Response( - { - "user": UserSerializer(request.user).data, - "slug": None, - "workspace_invites": workspace_invites, - "assigned_issues": assigned_issues, - }, + serialized_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, diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index f6c4ed87d..bdb758ac9 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -37,18 +37,19 @@ from plane.db.models import ( State, TeamMember, ProjectFavorite, + ProjectIdentifier, + Module, + Cycle, + CycleFavorite, + ModuleFavorite, + PageFavorite, + IssueViewFavorite, + Page, + IssueAssignee, + ModuleMember, ) -from plane.db.models import ( - Project, - ProjectMember, - Workspace, - ProjectMemberInvite, - User, - ProjectIdentifier, - Cycle, - Module, -) + from plane.bgtasks.project_invitation_task import project_invitation @@ -133,12 +134,12 @@ class ProjectViewSet(BaseViewSet): if serializer.is_valid(): serializer.save() - ## Add the user as Administrator to the project + # Add the user as Administrator to the project ProjectMember.objects.create( project_id=serializer.data["id"], member=request.user, role=20 ) - ## Default states + # Default states states = [ { "name": "Backlog", @@ -373,7 +374,7 @@ class UserProjectInvitationsViewset(BaseViewSet): ] ) - ## Delete joined project invites + # Delete joined project invites project_invitations.delete() return Response(status=status.HTTP_200_OK) @@ -411,14 +412,23 @@ class ProjectMemberViewSet(BaseViewSet): def partial_update(self, request, slug, project_id, pk): try: - project_member = ProjectMember.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) + project_member = ProjectMember.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id + ) if request.user.id == project_member.member_id: return Response( {"error": "You cannot update your own role"}, status=status.HTTP_400_BAD_REQUEST, ) - - if request.data.get("role", 10) > project_member.role: + # Check while updating user roles + requested_project_member = ProjectMember.objects.get( + project_id=project_id, workspace__slug=slug, member=request.user + ) + if ( + "role" in request.data + and int(request.data.get("role", project_member.role)) + > requested_project_member.role + ): return Response( { "error": "You cannot update a role that is higher than your own role" @@ -441,8 +451,70 @@ class ProjectMemberViewSet(BaseViewSet): ) except Exception as e: capture_exception(e) - return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + def destroy(self, request, slug, project_id, pk): + try: + project_member = ProjectMember.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + # check requesting user role + requesting_project_member = ProjectMember.objects.get( + workspace__slug=slug, member=request.user, project_id=project_id + ) + if requesting_project_member.role < project_member.role: + return Response( + {"error": "You cannot remove a user having role higher than yourself"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Remove all favorites + ProjectFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + CycleFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + ModuleFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + PageFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + IssueViewFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + # Also remove issue from issue assigned + IssueAssignee.objects.filter( + workspace__slug=slug, + project_id=project_id, + assignee=project_member.member, + ).delete() + + # Remove if module member + ModuleMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + member=project_member.member, + ).delete() + # Delete owned Pages + Page.objects.filter( + workspace__slug=slug, + project_id=project_id, + owned_by=project_member.member, + ).delete() + project_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except ProjectMember.DoesNotExist: + return Response( + {"error": "Project Member does not exist"}, status=status.HTTP_400 + ) + except Exception as e: + capture_exception(e) + return Response({"error": "Something went wrong please try again later"}) class AddMemberToProjectEndpoint(BaseAPIView): diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py index 823a1fcc8..88dddc43c 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/api/views/search.py @@ -210,13 +210,15 @@ class IssueSearchEndpoint(BaseAPIView): blocker_blocked_by = request.query_params.get("blocker_blocked_by", False) issue_id = request.query_params.get("issue_id", False) - issues = search_issues(query) - issues = issues.filter( + issues = Issue.objects.filter( workspace__slug=slug, project_id=project_id, project__project_projectmember__member=self.request.user, ) + if query: + issues = search_issues(query, issues) + if parent == "true" and issue_id: issue = Issue.objects.get(pk=issue_id) issues = issues.filter( @@ -227,7 +229,12 @@ class IssueSearchEndpoint(BaseAPIView): ) ) if blocker_blocked_by == "true" and issue_id: - issues = issues.filter(blocker_issues=issue_id, blocked_issues=issue_id) + issue = Issue.objects.get(pk=issue_id) + issues = issues.filter( + ~Q(pk=issue_id), + ~Q(blocked_issues__block=issue), + ~Q(blocker_issues__blocked_by=issue), + ) return Response( issues.values( diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index dcb8941a1..2f3fcb558 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -50,6 +50,14 @@ from plane.db.models import ( IssueActivity, Issue, WorkspaceTheme, + IssueAssignee, + ProjectFavorite, + CycleFavorite, + ModuleMember, + ModuleFavorite, + PageFavorite, + Page, + IssueViewFavorite, ) from plane.api.permissions import WorkSpaceBasePermission, WorkSpaceAdminPermission from plane.bgtasks.workspace_invitation_task import workspace_invitation @@ -353,7 +361,7 @@ class WorkspaceInvitationsViewset(BaseViewSet): super() .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "workspace__owner") + .select_related("workspace", "workspace__owner", "created_by") ) @@ -366,7 +374,8 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet): super() .get_queryset() .filter(email=self.request.user.email) - .select_related("workspace", "workspace__owner") + .select_related("workspace", "workspace__owner", "created_by") + .annotate(total_members=Count("workspace__workspace_member")) ) def create(self, request): @@ -432,7 +441,17 @@ class WorkSpaceMemberViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - if request.data.get("role", 10) > workspace_member.role: + # Get the requested user role + requested_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + # Check if role is being updated + # One cannot update role higher than his own role + if ( + "role" in request.data + and int(request.data.get("role", workspace_member.role)) + > requested_workspace_member.role + ): return Response( { "error": "You cannot update a role that is higher than your own role" @@ -460,6 +479,69 @@ class WorkSpaceMemberViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) + def destroy(self, request, slug, pk): + try: + # Check the user role who is deleting the user + workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, pk=pk) + + # check requesting user role + requesting_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + if requesting_workspace_member.role < workspace_member.role: + return Response( + {"error": "You cannot remove a user having role higher than you"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Delete the user also from all the projects + ProjectMember.objects.filter( + workspace__slug=slug, member=workspace_member.member + ).delete() + # Remove all favorites + ProjectFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + CycleFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + ModuleFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + PageFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + IssueViewFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + # Also remove issue from issue assigned + IssueAssignee.objects.filter( + workspace__slug=slug, assignee=workspace_member.member + ).delete() + + # Remove if module member + ModuleMember.objects.filter( + workspace__slug=slug, member=workspace_member.member + ).delete() + # Delete owned Pages + Page.objects.filter( + workspace__slug=slug, owned_by=workspace_member.member + ).delete() + + workspace_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except WorkspaceMember.DoesNotExist: + return Response( + {"error": "Workspace Member 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, + ) + class TeamMemberViewSet(BaseViewSet): serializer_class = TeamSerializer diff --git a/apiserver/plane/bgtasks/email_verification_task.py b/apiserver/plane/bgtasks/email_verification_task.py index 1da3a7510..89551044b 100644 --- a/apiserver/plane/bgtasks/email_verification_task.py +++ b/apiserver/plane/bgtasks/email_verification_task.py @@ -19,7 +19,7 @@ def email_verification(first_name, email, token, current_site): try: realtivelink = "/request-email-verification/" + "?token=" + str(token) - abs_url = "http://" + current_site + realtivelink + abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index f13f1b89a..687e4f976 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -16,12 +16,12 @@ from plane.db.models import User def forgot_password(first_name, email, uidb64, token, current_site): try: - realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/" - abs_url = "http://" + current_site + realtivelink + realtivelink = f"/reset-password/?uidb64={uidb64}&token={token}" + abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM - subject = f"Verify your Email!" + subject = f"Reset Your Password - Plane" context = { "first_name": first_name, diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index 00a4e6807..29851c435 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -13,7 +13,7 @@ from sentry_sdk import capture_exception def magic_link(email, key, token, current_site): try: realtivelink = f"/magic-sign-in/?password={token}&key={key}" - abs_url = "http://" + current_site + realtivelink + abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index 2015ffe5e..7f1125f80 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -21,7 +21,7 @@ def project_invitation(email, project_id, token, current_site): ) relativelink = f"/project-member-invitation/{project_member_invite.id}" - abs_url = "http://" + current_site + relativelink + abs_url = current_site + relativelink from_email_string = settings.EMAIL_FROM diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index 0ce32eee0..7b2bada0a 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -23,9 +23,9 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): ) realtivelink = ( - f"/workspace-member-invitation/{workspace_member_invite.id}?email={email}" + f"/workspace-member-invitation/?invitation_id={workspace_member_invite.id}&email={email}" ) - abs_url = "http://" + current_site + realtivelink + abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py index acbb9428f..01ef1d9d8 100644 --- a/apiserver/plane/db/models/asset.py +++ b/apiserver/plane/db/models/asset.py @@ -4,6 +4,7 @@ from uuid import uuid4 # Django import from django.db import models from django.core.exceptions import ValidationError +from django.conf import settings # Module import from . import BaseModel @@ -16,8 +17,7 @@ def get_upload_path(instance, filename): def file_size(value): - limit = 5 * 1024 * 1024 - if value.size > limit: + if value.size > settings.FILE_SIZE_LIMIT: raise ValidationError("File too large. Size should not exceed 5 MB.") diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 6e264566d..e25695c42 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -210,8 +210,8 @@ def get_upload_path(instance, filename): def file_size(value): - limit = 5 * 1024 * 1024 - if value.size > limit: + # File limit check is only for cloud hosted + if value.size > settings.FILE_SIZE_LIMIT: raise ValidationError("File too large. Size should not exceed 5 MB.") diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index e03a0b822..20b257a27 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -25,7 +25,13 @@ DATABASES = { } } -DOCKERIZED = os.environ.get("DOCKERIZED", False) +DOCKERIZED = int(os.environ.get( + "DOCKERIZED", 0 +)) == 1 + +USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 + +FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) if DOCKERIZED: DATABASES["default"] = dj_database_url.config() @@ -68,7 +74,7 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") if DOCKERIZED: REDIS_URL = os.environ.get("REDIS_URL") -WEB_URL = os.environ.get("WEB_URL", "localhost:3000") +WEB_URL = os.environ.get("WEB_URL", "http://localhost:3000") PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) @@ -84,5 +90,4 @@ LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False) CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL") CELERY_BROKER_URL = os.environ.get("REDIS_URL") - -GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) \ No newline at end of file +GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index e58736472..7e7f4186f 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -29,9 +29,12 @@ DATABASES = { DATABASES["default"] = dj_database_url.config() SITE_ID = 1 -DOCKERIZED = os.environ.get( - "DOCKERIZED", False -) # Set the variable true if running in docker-compose environment +# Set the variable true if running in docker environment +DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1 + +USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 + +FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) # Enable Connection Pooling (if desired) # DATABASES['default']['ENGINE'] = 'django_postgrespool' @@ -69,7 +72,7 @@ CORS_ALLOW_CREDENTIALS = True # Simplified static file serving. STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" -if os.environ.get("SENTRY_DSN", False): +if bool(os.environ.get("SENTRY_DSN", False)): sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN", ""), integrations=[DjangoIntegration(), RedisIntegration()], @@ -80,12 +83,27 @@ if os.environ.get("SENTRY_DSN", False): environment="production", ) -if ( - os.environ.get("AWS_REGION", False) - and os.environ.get("AWS_ACCESS_KEY_ID", False) - and os.environ.get("AWS_SECRET_ACCESS_KEY", False) - and os.environ.get("AWS_S3_BUCKET_NAME", False) -): +if DOCKERIZED and USE_MINIO: + INSTALLED_APPS += ("storages",) + DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" + # The AWS access key to use. + AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") + # The AWS secret access key to use. + AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key") + # The name of the bucket to store files in. + AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") + # The full URL to the S3 endpoint. Leave blank to use the default region URL. + AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "http://plane-minio:9000") + # Default permissions + AWS_DEFAULT_ACL = "public-read" + AWS_QUERYSTRING_AUTH = False + AWS_S3_FILE_OVERWRITE = False + + # Custom Domain settings + parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost")) + AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}" + AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:" +else: # The AWS region to connect to. AWS_REGION = os.environ.get("AWS_REGION", "") @@ -99,7 +117,7 @@ if ( # AWS_SESSION_TOKEN = "" # The name of the bucket to store files in. - AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "") + AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME") # How to construct S3 URLs ("auto", "path", "virtual"). AWS_S3_ADDRESSING_STYLE = "auto" @@ -166,14 +184,8 @@ if ( # extra characters appended. AWS_S3_FILE_OVERWRITE = False - # AWS Settings End - DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage" - -else: - MEDIA_URL = "/uploads/" - MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") - +# AWS Settings End # Enable Connection Pooling (if desired) # DATABASES['default']['ENGINE'] = 'django_postgrespool' @@ -218,14 +230,8 @@ else: } } -RQ_QUEUES = { - "default": { - "USE_REDIS_CACHE": "default", - } -} - -WEB_URL = os.environ.get("WEB_URL") +WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so") PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index d4d0e5e12..c6ffcaf22 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -49,6 +49,12 @@ CORS_ALLOW_ALL_ORIGINS = True # Simplified static file serving. STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" +# Make true if running in a docker environment +DOCKERIZED = int(os.environ.get( + "DOCKERIZED", 0 +)) == 1 +FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) +USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN"), @@ -165,7 +171,6 @@ CSRF_COOKIE_SECURE = True REDIS_URL = os.environ.get("REDIS_URL") -DOCKERIZED = os.environ.get("DOCKERIZED", False) CACHES = { "default": { diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index 3dfde38bd..a2244ffe0 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -7,7 +7,7 @@ from django.urls import path from django.views.generic import TemplateView from django.conf import settings -from django.conf.urls import include, url +from django.conf.urls import include, url, static # from django.conf.urls.static import static @@ -17,9 +17,8 @@ urlpatterns = [ path("api/", include("plane.api.urls")), path("", include("plane.web.urls")), ] -# + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -# + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +urlpatterns = urlpatterns + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: import debug_toolbar diff --git a/apiserver/plane/utils/issue_search.py b/apiserver/plane/utils/issue_search.py index 93b0df6da..40f85dde4 100644 --- a/apiserver/plane/utils/issue_search.py +++ b/apiserver/plane/utils/issue_search.py @@ -8,7 +8,7 @@ from django.db.models import Q from plane.db.models import Issue -def search_issues(query): +def search_issues(query, queryset): fields = ["name", "sequence_id"] q = Q() for field in fields: @@ -18,6 +18,6 @@ def search_issues(query): q |= Q(**{"sequence_id": sequence_id}) else: q |= Q(**{f"{field}__icontains": query}) - return Issue.objects.filter( + return queryset.filter( q, ).distinct() diff --git a/apiserver/requirements/production.txt b/apiserver/requirements/production.txt index 2547ce255..c37e98ffd 100644 --- a/apiserver/requirements/production.txt +++ b/apiserver/requirements/production.txt @@ -4,7 +4,7 @@ dj-database-url==1.2.0 gunicorn==20.1.0 whitenoise==6.3.0 django-storages==1.13.2 -boto==2.49.0 +boto3==1.26.136 django-anymail==9.0 twilio==7.16.2 django-debug-toolbar==3.8.1 diff --git a/apiserver/templates/emails/auth/forgot_password.html b/apiserver/templates/emails/auth/forgot_password.html index 7c3ae585f..76b8903d7 100644 --- a/apiserver/templates/emails/auth/forgot_password.html +++ b/apiserver/templates/emails/auth/forgot_password.html @@ -1,11 +1,21 @@ -

+ + +

Dear {{first_name}},

- Welcome! Your account has been created. - Verify your email by clicking on the link below
- {{forgot_password_url}} - successfully.

-

+ We received a request to reset your password for your Plane account. +

+ To proceed with resetting your password, please click on the link below: +
+ {{forgot_password_url}} +

+ If you didn't request to reset your password, please ignore this email. Your account will remain secure. +

+ If you have any questions or need further assistance, please contact our support team. +

+ Thank you for using Plane. +

+ \ No newline at end of file diff --git a/app.json b/app.json index 7f6b27427..bc5789078 100644 --- a/app.json +++ b/app.json @@ -37,6 +37,14 @@ "description": "Email host to send emails from", "value": "" }, + "EMAIL_FROM": { + "description": "Email Sender", + "value": "" + }, + "EMAIL_PORT": { + "description": "The default Email PORT to use", + "value": "587" + }, "AWS_REGION": { "description": "AWS Region to use for S3", "value": "false" @@ -49,30 +57,22 @@ "description": "AWS Secret Access Key to use for S3", "value": "" }, - "SENTRY_DSN": { - "description": "", - "value": "" - }, "AWS_S3_BUCKET_NAME": { "description": "AWS Bucket Name to use for S3", "value": "" }, + "SENTRY_DSN": { + "description": "", + "value": "" + }, "WEB_URL": { - "description": "Web URL for Plane", + "description": "Web URL for Plane this will be used for redirections in the emails", "value": "" }, "GITHUB_CLIENT_SECRET": { "description": "Github Client Secret", "value": "" }, - "NEXT_PUBLIC_GITHUB_ID": { - "description": "Next Public Github ID", - "value": "" - }, - "NEXT_PUBLIC_GOOGLE_CLIENTID": { - "description": "Next Public Google Client ID", - "value": "" - }, "NEXT_PUBLIC_API_BASE_URL": { "description": "Next Public API Base URL", "value": "" diff --git a/apps/app/.eslintrc.js b/apps/app/.eslintrc.js index c8df60750..38e6a5f4c 100644 --- a/apps/app/.eslintrc.js +++ b/apps/app/.eslintrc.js @@ -1,4 +1,7 @@ module.exports = { root: true, extends: ["custom"], + rules: { + "@next/next/no-img-element": "off", + }, }; diff --git a/apps/app/Dockerfile.dev b/apps/app/Dockerfile.dev index 7b802634c..d4281c9a3 100644 --- a/apps/app/Dockerfile.dev +++ b/apps/app/Dockerfile.dev @@ -1,6 +1,5 @@ FROM node:18-alpine RUN apk add --no-cache libc6-compat -RUN apk update # Set working directory WORKDIR /app diff --git a/apps/app/Dockerfile.web b/apps/app/Dockerfile.web index 0b3e45f7a..1b9bc41d5 100644 --- a/apps/app/Dockerfile.web +++ b/apps/app/Dockerfile.web @@ -1,6 +1,5 @@ FROM node:18-alpine AS builder RUN apk add --no-cache libc6-compat -RUN apk update # Set working directory WORKDIR /app ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER @@ -14,7 +13,6 @@ RUN turbo prune --scope=app --docker FROM node:18-alpine AS installer RUN apk add --no-cache libc6-compat -RUN apk update WORKDIR /app ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index 48288f77e..9132ee994 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -21,6 +21,7 @@ export const EmailCodeForm = ({ onSuccess }: any) => { const [codeResent, setCodeResent] = useState(false); const [isCodeResending, setIsCodeResending] = useState(false); const [errorResendingCode, setErrorResendingCode] = useState(false); + const [isLoading, setIsLoading] = useState(false); const { setToastAlert } = useToast(); const { timer: resendCodeTimer, setTimer: setResendCodeTimer } = useTimer(); @@ -64,22 +65,19 @@ export const EmailCodeForm = ({ onSuccess }: any) => { }; const handleSignin = async (formData: EmailCodeFormValues) => { - await authenticationService - .magicSignIn(formData) - .then((response) => { - onSuccess(response); - }) - .catch((error) => { - setToastAlert({ - title: "Oops!", - type: "error", - message: error?.response?.data?.error ?? "Enter the correct code to sign in", - }); - setError("token" as keyof EmailCodeFormValues, { - type: "manual", - message: error.error, - }); + setIsLoading(true); + await authenticationService.magicSignIn(formData).catch((error) => { + setIsLoading(false); + setToastAlert({ + title: "Oops!", + type: "error", + message: error?.response?.data?.error ?? "Enter the correct code to sign in", }); + setError("token" as keyof EmailCodeFormValues, { + type: "manual", + message: error.error, + }); + }); }; const emailOld = getValues("email"); @@ -88,6 +86,25 @@ export const EmailCodeForm = ({ onSuccess }: any) => { setErrorResendingCode(false); }, [emailOld]); + useEffect(() => { + const submitForm = (e: KeyboardEvent) => { + if (!codeSent && e.key === "Enter") { + e.preventDefault(); + handleSubmit(onSubmit)().then(() => { + setResendCodeTimer(30); + }); + } + }; + + if (!codeSent) { + window.addEventListener("keydown", submitForm); + } + + return () => { + window.removeEventListener("keydown", submitForm); + }; + }, [handleSubmit, codeSent]); + return ( <>
@@ -177,9 +194,9 @@ export const EmailCodeForm = ({ onSuccess }: any) => { size="md" onClick={handleSubmit(handleSignin)} disabled={!isValid && isDirty} - loading={isSubmitting} + loading={isLoading} > - {isSubmitting ? "Signing in..." : "Sign in"} + {isLoading ? "Signing in..." : "Sign in"} ) : ( { +export const EmailPasswordForm = ({ handleSignIn }: any) => { + const [isResettingPassword, setIsResettingPassword] = useState(false); + const { setToastAlert } = useToast(); + const { register, handleSubmit, @@ -38,7 +41,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => { authenticationService .emailLogin(formData) .then((response) => { - onSuccess(response); + if (handleSignIn) handleSignIn(response); }) .catch((error) => { console.log(error); @@ -58,59 +61,66 @@ export const EmailPasswordForm = ({ onSuccess }: any) => { }); }); }; + return ( <> - -
- - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - value - ) || "Email ID is not valid", - }} - error={errors.email} - placeholder="Enter your Email ID" - /> -
-
- -
-
-
- - - Forgot your password? - - + {isResettingPassword ? ( + + ) : ( + +
+ + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email ID is not valid", + }} + error={errors.email} + placeholder="Enter your Email ID" + />
-
-
- - {isSubmitting ? "Signing in..." : "Sign In"} - -
- +
+ +
+
+
+ +
+
+
+ + {isSubmitting ? "Signing in..." : "Sign In"} + +
+ + )} ); }; diff --git a/apps/app/components/account/email-reset-password-form.tsx b/apps/app/components/account/email-reset-password-form.tsx new file mode 100644 index 000000000..03ea69042 --- /dev/null +++ b/apps/app/components/account/email-reset-password-form.tsx @@ -0,0 +1,93 @@ +import React from "react"; + +// react hook form +import { useForm } from "react-hook-form"; +// services +import userService from "services/user.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Input, PrimaryButton, SecondaryButton } from "components/ui"; +// types +type Props = { + setIsResettingPassword: React.Dispatch>; +}; + +export const EmailResetPasswordForm: React.FC = ({ setIsResettingPassword }) => { + const { setToastAlert } = useToast(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + email: "", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const forgotPassword = async (formData: any) => { + const payload = { + email: formData.email, + }; + + await userService + .forgotPassword(payload) + .then(() => + setToastAlert({ + type: "success", + title: "Success!", + message: "Password reset link has been sent to your email address.", + }) + ) + .catch((err) => { + if (err.status === 400) + setToastAlert({ + type: "error", + title: "Error!", + message: "Please check the Email ID entered.", + }); + else + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again.", + }); + }); + }; + + return ( +
+
+ + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email ID is not valid", + }} + error={errors.email} + placeholder="Enter registered Email ID" + /> +
+
+ setIsResettingPassword(false)} + > + Go Back + + + {isSubmitting ? "Sending link..." : "Send reset link"} + +
+
+ ); +}; diff --git a/apps/app/components/account/email-signin-form.tsx b/apps/app/components/account/email-signin-form.tsx deleted file mode 100644 index e2f81d50c..000000000 --- a/apps/app/components/account/email-signin-form.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useState, FC } from "react"; -import { KeyIcon } from "@heroicons/react/24/outline"; -// components -import { EmailCodeForm, EmailPasswordForm } from "components/account"; - -export interface EmailSignInFormProps { - handleSuccess: () => void; -} - -export const EmailSignInForm: FC = (props) => { - const { handleSuccess } = props; - // states - const [useCode, setUseCode] = useState(true); - - return ( - <> - {useCode ? ( - - ) : ( - - )} - - ); -}; diff --git a/apps/app/components/account/github-login-button.tsx b/apps/app/components/account/github-login-button.tsx index 16b9743d5..764680d30 100644 --- a/apps/app/components/account/github-login-button.tsx +++ b/apps/app/components/account/github-login-button.tsx @@ -29,7 +29,7 @@ export const GithubLoginButton: FC = (props) => { useEffect(() => { const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - setLoginCallBackURL(`${origin}/signin` as any); + setLoginCallBackURL(`${origin}/` as any); }, []); return ( diff --git a/apps/app/components/account/index.ts b/apps/app/components/account/index.ts index b78e2e921..dee3e3251 100644 --- a/apps/app/components/account/index.ts +++ b/apps/app/components/account/index.ts @@ -1,5 +1,5 @@ -export * from "./google-login"; export * from "./email-code-form"; export * from "./email-password-form"; +export * from "./email-reset-password-form"; export * from "./github-login-button"; -export * from "./email-signin-form"; +export * from "./google-login"; diff --git a/apps/app/components/analytics/custom-analytics/custom-analytics.tsx b/apps/app/components/analytics/custom-analytics/custom-analytics.tsx index f9be7d1dd..f46b7f2b2 100644 --- a/apps/app/components/analytics/custom-analytics/custom-analytics.tsx +++ b/apps/app/components/analytics/custom-analytics/custom-analytics.tsx @@ -18,7 +18,7 @@ import { Loader, PrimaryButton } from "components/ui"; // helpers import { convertResponseToBarGraphData } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse } from "types"; +import { IAnalyticsParams, IAnalyticsResponse, ICurrentUserResponse } from "types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; @@ -29,6 +29,7 @@ type Props = { control: Control; setValue: UseFormSetValue; fullScreen: boolean; + user: ICurrentUserResponse | undefined; }; export const CustomAnalytics: React.FC = ({ @@ -38,6 +39,7 @@ export const CustomAnalytics: React.FC = ({ control, setValue, fullScreen, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -124,6 +126,7 @@ export const CustomAnalytics: React.FC = ({ params={params} fullScreen={fullScreen} isProjectLevel={isProjectLevel} + user={user} />
); diff --git a/apps/app/components/analytics/custom-analytics/sidebar.tsx b/apps/app/components/analytics/custom-analytics/sidebar.tsx index e9a9d5d7f..5f4700f29 100644 --- a/apps/app/components/analytics/custom-analytics/sidebar.tsx +++ b/apps/app/components/analytics/custom-analytics/sidebar.tsx @@ -7,6 +7,7 @@ import analyticsService from "services/analytics.service"; import projectService from "services/project.service"; import cyclesService from "services/cycles.service"; import modulesService from "services/modules.service"; +import trackEventServices from "services/track-event.service"; // hooks import useProjects from "hooks/use-projects"; import useToast from "hooks/use-toast"; @@ -23,7 +24,14 @@ import { ContrastIcon, LayerDiagonalIcon } from "components/icons"; // helpers import { renderShortDate } from "helpers/date-time.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IProject } from "types"; +import { + IAnalyticsParams, + IAnalyticsResponse, + ICurrentUserResponse, + IExportAnalyticsFormData, + IProject, + IWorkspace, +} from "types"; // fetch-keys import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; // constants @@ -34,6 +42,7 @@ type Props = { params: IAnalyticsParams; fullScreen: boolean; isProjectLevel: boolean; + user: ICurrentUserResponse | undefined; }; export const AnalyticsSidebar: React.FC = ({ @@ -41,6 +50,7 @@ export const AnalyticsSidebar: React.FC = ({ params, fullScreen, isProjectLevel = false, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -82,6 +92,60 @@ export const AnalyticsSidebar: React.FC = ({ : null ); + const trackExportAnalytics = () => { + const eventPayload: any = { + workspaceSlug: workspaceSlug?.toString(), + params: { + x_axis: params.x_axis, + y_axis: params.y_axis, + group: params.segment, + project: params.project, + }, + }; + + if (projectDetails) { + const workspaceDetails = projectDetails.workspace as IWorkspace; + + eventPayload.workspaceId = workspaceDetails.id; + eventPayload.workspaceName = workspaceDetails.name; + eventPayload.projectId = projectDetails.id; + eventPayload.projectIdentifier = projectDetails.identifier; + eventPayload.projectName = projectDetails.name; + } + + if (cycleDetails || moduleDetails) { + const details = cycleDetails || moduleDetails; + + eventPayload.workspaceId = details?.workspace_detail?.id; + eventPayload.workspaceName = details?.workspace_detail?.name; + eventPayload.projectId = details?.project_detail.id; + eventPayload.projectIdentifier = details?.project_detail.identifier; + eventPayload.projectName = details?.project_detail.name; + } + + if (cycleDetails) { + eventPayload.cycleId = cycleDetails.id; + eventPayload.cycleName = cycleDetails.name; + } + + if (moduleDetails) { + eventPayload.moduleId = moduleDetails.id; + eventPayload.moduleName = moduleDetails.name; + } + + trackEventServices.trackAnalyticsEvent( + eventPayload, + cycleId + ? "CYCLE_ANALYTICS_EXPORT" + : moduleId + ? "MODULE_ANALYTICS_EXPORT" + : projectId + ? "PROJECT_ANALYTICS_EXPORT" + : "WORKSPACE_ANALYTICS_EXPORT", + user + ); + }; + const exportAnalytics = () => { if (!workspaceSlug) return; @@ -95,13 +159,15 @@ export const AnalyticsSidebar: React.FC = ({ analyticsService .exportAnalytics(workspaceSlug.toString(), data) - .then((res) => + .then((res) => { setToastAlert({ type: "success", title: "Success!", message: res.message, - }) - ) + }); + + trackExportAnalytics(); + }) .catch(() => setToastAlert({ type: "error", diff --git a/apps/app/components/analytics/project-modal.tsx b/apps/app/components/analytics/project-modal.tsx index c2072642c..da308582f 100644 --- a/apps/app/components/analytics/project-modal.tsx +++ b/apps/app/components/analytics/project-modal.tsx @@ -13,6 +13,7 @@ import analyticsService from "services/analytics.service"; import projectService from "services/project.service"; import cyclesService from "services/cycles.service"; import modulesService from "services/modules.service"; +import trackEventServices from "services/track-event.service"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; // icons @@ -22,9 +23,10 @@ import { XMarkIcon, } from "@heroicons/react/24/outline"; // types -import { IAnalyticsParams } from "types"; +import { IAnalyticsParams, IWorkspace } from "types"; // fetch-keys import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; +import useUserAuth from "hooks/use-user-auth"; type Props = { isOpen: boolean; @@ -46,6 +48,8 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { user } = useUserAuth(); + const { control, watch, setValue } = useForm({ defaultValues }); const params: IAnalyticsParams = { @@ -95,6 +99,51 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { : null ); + const trackAnalyticsEvent = (tab: string) => { + const eventPayload: any = { + workspaceSlug: workspaceSlug?.toString(), + }; + + if (projectDetails) { + const workspaceDetails = projectDetails.workspace as IWorkspace; + + eventPayload.workspaceId = workspaceDetails.id; + eventPayload.workspaceName = workspaceDetails.name; + eventPayload.projectId = projectDetails.id; + eventPayload.projectIdentifier = projectDetails.identifier; + eventPayload.projectName = projectDetails.name; + } + + if (cycleDetails || moduleDetails) { + const details = cycleDetails || moduleDetails; + + eventPayload.workspaceId = details?.workspace_detail?.id; + eventPayload.workspaceName = details?.workspace_detail?.name; + eventPayload.projectId = details?.project_detail.id; + eventPayload.projectIdentifier = details?.project_detail.identifier; + eventPayload.projectName = details?.project_detail.name; + } + + if (cycleDetails) { + eventPayload.cycleId = cycleDetails.id; + eventPayload.cycleName = cycleDetails.name; + } + + if (moduleDetails) { + eventPayload.moduleId = moduleDetails.id; + eventPayload.moduleName = moduleDetails.name; + } + + const eventType = + tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS"; + + trackEventServices.trackAnalyticsEvent( + eventPayload, + cycleId ? `CYCLE_${eventType}` : moduleId ? `MODULE_${eventType}` : `PROJECT_${eventType}`, + user + ); + }; + const handleClose = () => { onClose(); }; @@ -146,6 +195,7 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { selected ? "bg-brand-surface-2" : "" }` } + onClick={() => trackAnalyticsEvent(tab)} > {tab} @@ -164,6 +214,7 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { control={control} setValue={setValue} fullScreen={fullScreen} + user={user} /> diff --git a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx index 9e28d0f3e..855f9eff4 100644 --- a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx +++ b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx @@ -1,5 +1,3 @@ -import Image from "next/image"; - type Props = { users: { avatar: string | null; @@ -23,12 +21,10 @@ export const AnalyticsLeaderboard: React.FC = ({ users, title }) => ( >
{user && user.avatar && user.avatar !== "" ? ( -
- + {user.email
diff --git a/apps/app/components/auth-screens/not-authorized-view.tsx b/apps/app/components/auth-screens/not-authorized-view.tsx index dc1db8a21..d76e56b61 100644 --- a/apps/app/components/auth-screens/not-authorized-view.tsx +++ b/apps/app/components/auth-screens/not-authorized-view.tsx @@ -21,12 +21,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { const { asPath: currentPath } = useRouter(); return ( - +
= ({ actionButton, type }) => { {user ? (

You have signed in as {user.email}.
- + Sign in {" "} with different account that has access to this page. @@ -52,7 +47,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { ) : (

You need to{" "} - + Sign in {" "} with an account that has access to this page. diff --git a/apps/app/components/auth-screens/workspace/not-a-member.tsx b/apps/app/components/auth-screens/workspace/not-a-member.tsx index 9627143ba..12bb3d850 100644 --- a/apps/app/components/auth-screens/workspace/not-a-member.tsx +++ b/apps/app/components/auth-screens/workspace/not-a-member.tsx @@ -1,44 +1,34 @@ import Link from "next/link"; -import { useRouter } from "next/router"; // layouts import DefaultLayout from "layouts/default-layout"; // ui import { PrimaryButton, SecondaryButton } from "components/ui"; -export const NotAWorkspaceMember = () => { - const router = useRouter(); - - return ( - -

-
-
-

Not Authorized!

-

- You{"'"}re not a member of this workspace. Please contact the workspace admin to get - an invitation or check your pending invitations. -

-
- +export const NotAWorkspaceMember = () => ( + +
+
+
+

Not Authorized!

+

+ You{"'"}re not a member of this workspace. Please contact the workspace admin to get an + invitation or check your pending invitations. +

+
+
- - ); -}; +
+
+); diff --git a/apps/app/components/breadcrumbs/index.tsx b/apps/app/components/breadcrumbs/index.tsx index 98944fe37..240faefa2 100644 --- a/apps/app/components/breadcrumbs/index.tsx +++ b/apps/app/components/breadcrumbs/index.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import Link from "next/link"; // icons import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { Icon } from "components/ui"; type BreadcrumbsProps = { children: any; @@ -16,10 +17,13 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
{children}
diff --git a/apps/app/components/command-palette/change-issue-assignee.tsx b/apps/app/components/command-palette/change-issue-assignee.tsx index 09f597e2e..56351335e 100644 --- a/apps/app/components/command-palette/change-issue-assignee.tsx +++ b/apps/app/components/command-palette/change-issue-assignee.tsx @@ -7,7 +7,7 @@ import { Command } from "cmdk"; // services import issuesService from "services/issues.service"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // constants import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, PROJECT_MEMBERS } from "constants/fetch-keys"; // icons @@ -18,9 +18,10 @@ import { Avatar } from "components/ui"; type Props = { setIsPaletteOpen: Dispatch>; issue: IIssue; + user: ICurrentUserResponse | undefined; }; -export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue }) => { +export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue, user }) => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -57,18 +58,21 @@ export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue } async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + async (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...formData, + }; + }, false ); const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); }) @@ -80,7 +84,7 @@ export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue } ); const handleIssueAssignees = (assignee: string) => { - const updatedAssignees = issue.assignees ?? []; + const updatedAssignees = issue.assignees_list ?? []; if (updatedAssignees.includes(assignee)) { updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); diff --git a/apps/app/components/command-palette/change-issue-priority.tsx b/apps/app/components/command-palette/change-issue-priority.tsx index b6eca1df8..2db03268d 100644 --- a/apps/app/components/command-palette/change-issue-priority.tsx +++ b/apps/app/components/command-palette/change-issue-priority.tsx @@ -7,7 +7,7 @@ import { Command } from "cmdk"; // services import issuesService from "services/issues.service"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // constants import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; import { PRIORITIES } from "constants/project"; @@ -17,9 +17,10 @@ import { CheckIcon, getPriorityIcon } from "components/icons"; type Props = { setIsPaletteOpen: Dispatch>; issue: IIssue; + user: ICurrentUserResponse; }; -export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue }) => { +export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue, user }) => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -27,18 +28,22 @@ export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue } async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + async (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + ...formData, + }; + }, false ); const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); }) diff --git a/apps/app/components/command-palette/change-issue-state.tsx b/apps/app/components/command-palette/change-issue-state.tsx index 30ab68b63..0378df878 100644 --- a/apps/app/components/command-palette/change-issue-state.tsx +++ b/apps/app/components/command-palette/change-issue-state.tsx @@ -12,7 +12,7 @@ import { getStatesList } from "helpers/state.helper"; import issuesService from "services/issues.service"; import stateService from "services/state.service"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // fetch keys import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys"; // icons @@ -21,9 +21,10 @@ import { CheckIcon, getStateGroupIcon } from "components/icons"; type Props = { setIsPaletteOpen: Dispatch>; issue: IIssue; + user: ICurrentUserResponse | undefined; }; -export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue }) => { +export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue, user }) => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -39,18 +40,21 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue }) = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + async (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...formData, + }; + }, false ); const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { mutateIssueDetails(); mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index bed84f5ad..ff889898e 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -120,18 +120,23 @@ export const CommandPalette: React.FC = () => { async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + ...formData, + }; + }, false ); const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); mutate(ISSUE_DETAILS(issueId as string)); @@ -325,25 +330,33 @@ export const CommandPalette: React.FC = () => { <> {workspaceSlug && ( - + )} {projectId && ( <> setIsCreateCycleModalOpen(false)} + user={user} /> setIsCreateViewModalOpen(false)} isOpen={isCreateViewModalOpen} + user={user} /> setIsCreateUpdatePageModalOpen(false)} + user={user} /> )} @@ -352,6 +365,7 @@ export const CommandPalette: React.FC = () => { handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueDetails} + user={user} /> )} @@ -362,6 +376,7 @@ export const CommandPalette: React.FC = () => { { )} @@ -856,12 +872,14 @@ export const CommandPalette: React.FC = () => { )} {page === "change-issue-assignee" && issueDetails && ( )} {page === "change-interface-theme" && ( diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx index 495cee0a5..3e67e86b5 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -5,7 +5,7 @@ import { SingleBoard } from "components/core/board-view/single-board"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IState, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; import { getStateGroupIcon } from "components/icons"; type Props = { @@ -19,6 +19,7 @@ type Props = { handleTrashBox: (isDragging: boolean) => void; removeIssue: ((bridgeId: string, issueId: string) => void) | null; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -33,6 +34,7 @@ export const AllBoards: React.FC = ({ handleTrashBox, removeIssue, isCompleted = false, + user, userAuth, }) => { const { @@ -65,6 +67,7 @@ export const AllBoards: React.FC = ({ handleTrashBox={handleTrashBox} removeIssue={removeIssue} isCompleted={isCompleted} + user={user} userAuth={userAuth} /> ); diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/board-view/single-board.tsx index 16fe0e887..f7816cd5b 100644 --- a/apps/app/components/core/board-view/single-board.tsx +++ b/apps/app/components/core/board-view/single-board.tsx @@ -17,7 +17,7 @@ import { PlusIcon } from "@heroicons/react/24/outline"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // types -import { IIssue, IState, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; type Props = { type?: "issue" | "cycle" | "module"; @@ -31,6 +31,7 @@ type Props = { handleTrashBox: (isDragging: boolean) => void; removeIssue: ((bridgeId: string, issueId: string) => void) | null; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -46,6 +47,7 @@ export const SingleBoard: React.FC = ({ handleTrashBox, removeIssue, isCompleted = false, + user, userAuth, }) => { // collapse/expand @@ -129,6 +131,7 @@ export const SingleBoard: React.FC = ({ removeIssue(issue.bridge_id, issue.id); }} isCompleted={isCompleted} + user={user} userAuth={userAuth} /> )} diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index 7f8a89aa2..e2d530153 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -44,7 +44,7 @@ import { LayerDiagonalIcon } from "components/icons"; import { handleIssuesMutation } from "constants/issue"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types -import { IIssue, Properties, TIssueGroupByOptions, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, Properties, TIssueGroupByOptions, UserAuth } from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -69,6 +69,7 @@ type Props = { handleDeleteIssue: (issue: IIssue) => void; handleTrashBox: (isDragging: boolean) => void; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -87,6 +88,7 @@ export const SingleBoardIssue: React.FC = ({ handleDeleteIssue, handleTrashBox, isCompleted = false, + user, userAuth, }) => { // context menu @@ -170,7 +172,7 @@ export const SingleBoardIssue: React.FC = ({ } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData) + .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) .then(() => { if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); @@ -342,6 +344,7 @@ export const SingleBoardIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} + user={user} selfPositioned /> )} @@ -350,6 +353,7 @@ export const SingleBoardIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} + user={user} selfPositioned /> )} @@ -357,6 +361,7 @@ export const SingleBoardIssue: React.FC = ({ )} @@ -384,6 +389,7 @@ export const SingleBoardIssue: React.FC = ({ partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} tooltipPosition="left" + user={user} selfPositioned /> )} @@ -392,6 +398,7 @@ export const SingleBoardIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} + user={user} selfPositioned /> )} diff --git a/apps/app/components/core/bulk-delete-issues-modal.tsx b/apps/app/components/core/bulk-delete-issues-modal.tsx index 7fe2181f6..603efe8e3 100644 --- a/apps/app/components/core/bulk-delete-issues-modal.tsx +++ b/apps/app/components/core/bulk-delete-issues-modal.tsx @@ -18,7 +18,7 @@ import { DangerButton, SecondaryButton } from "components/ui"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // fetch keys import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; @@ -29,9 +29,10 @@ type FormInput = { type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; + user: ICurrentUserResponse | undefined; }; -export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => { +export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user }) => { const [query, setQuery] = useState(""); const router = useRouter(); @@ -91,9 +92,14 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => if (workspaceSlug && projectId) { await issuesServices - .bulkDeleteIssues(workspaceSlug as string, projectId as string, { - issue_ids: data.delete_issue_ids, - }) + .bulkDeleteIssues( + workspaceSlug as string, + projectId as string, + { + issue_ids: data.delete_issue_ids, + }, + user + ) .then((res) => { setToastAlert({ title: "Success", diff --git a/apps/app/components/core/calendar-view/calendar.tsx b/apps/app/components/core/calendar-view/calendar.tsx index ce453f898..fa29eb9f7 100644 --- a/apps/app/components/core/calendar-view/calendar.tsx +++ b/apps/app/components/core/calendar-view/calendar.tsx @@ -24,7 +24,7 @@ import { formatDate, } from "helpers/calendar.helper"; // types -import { ICalendarRange, IIssue, UserAuth } from "types"; +import { ICalendarRange, ICurrentUserResponse, IIssue, UserAuth } from "types"; // fetch-keys import { CYCLE_ISSUES_WITH_PARAMS, @@ -38,6 +38,7 @@ type Props = { handleDeleteIssue: (issue: IIssue) => void; addIssueToDate: (date: string) => void; isCompleted: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -46,6 +47,7 @@ export const CalendarView: React.FC = ({ handleDeleteIssue, addIssueToDate, isCompleted = false, + user, userAuth, }) => { const [showWeekEnds, setShowWeekEnds] = useState(false); @@ -134,9 +136,15 @@ export const CalendarView: React.FC = ({ ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, draggableId, { - target_date: destination?.droppableId, - }) + .patchIssue( + workspaceSlug as string, + projectId as string, + draggableId, + { + target_date: destination?.droppableId, + }, + user + ) .then(() => mutate(fetchKey)); }; @@ -219,6 +227,7 @@ export const CalendarView: React.FC = ({ addIssueToDate={addIssueToDate} isMonthlyView={isMonthlyView} showWeekEnds={showWeekEnds} + user={user} isNotAllowed={isNotAllowed} /> ))} diff --git a/apps/app/components/core/calendar-view/single-date.tsx b/apps/app/components/core/calendar-view/single-date.tsx index fd552188c..2dbfe5dfd 100644 --- a/apps/app/components/core/calendar-view/single-date.tsx +++ b/apps/app/components/core/calendar-view/single-date.tsx @@ -10,7 +10,7 @@ import { PlusSmallIcon } from "@heroicons/react/24/outline"; // helper import { formatDate } from "helpers/calendar.helper"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; type Props = { handleEditIssue: (issue: IIssue) => void; @@ -23,6 +23,7 @@ type Props = { addIssueToDate: (date: string) => void; isMonthlyView: boolean; showWeekEnds: boolean; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -34,6 +35,7 @@ export const SingleCalendarDate: React.FC = ({ addIssueToDate, isMonthlyView, showWeekEnds, + user, isNotAllowed, }) => { const [showAllIssues, setShowAllIssues] = useState(false); @@ -72,6 +74,7 @@ export const SingleCalendarDate: React.FC = ({ issue={issue} handleEditIssue={handleEditIssue} handleDeleteIssue={handleDeleteIssue} + user={user} isNotAllowed={isNotAllowed} /> )} diff --git a/apps/app/components/core/calendar-view/single-issue.tsx b/apps/app/components/core/calendar-view/single-issue.tsx index 9102f7338..4fa9def3b 100644 --- a/apps/app/components/core/calendar-view/single-issue.tsx +++ b/apps/app/components/core/calendar-view/single-issue.tsx @@ -28,7 +28,7 @@ import { LayerDiagonalIcon } from "components/icons"; // helper import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // type -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // fetch-keys import { CYCLE_ISSUES_WITH_PARAMS, @@ -44,6 +44,7 @@ type Props = { provided: DraggableProvided; snapshot: DraggableStateSnapshot; issue: IIssue; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -54,6 +55,7 @@ export const SingleCalendarIssue: React.FC = ({ provided, snapshot, issue, + user, isNotAllowed, }) => { const router = useRouter(); @@ -95,7 +97,7 @@ export const SingleCalendarIssue: React.FC = ({ ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user) .then(() => { mutate(fetchKey); }) @@ -183,6 +185,7 @@ export const SingleCalendarIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -192,6 +195,7 @@ export const SingleCalendarIssue: React.FC = ({ partialUpdateIssue={partialUpdateIssue} position="left" isNotAllowed={isNotAllowed} + user={user} /> )} @@ -199,6 +203,7 @@ export const SingleCalendarIssue: React.FC = ({ )} @@ -227,6 +232,7 @@ export const SingleCalendarIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -235,6 +241,7 @@ export const SingleCalendarIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + user={user} isNotAllowed={isNotAllowed} /> )} diff --git a/apps/app/components/core/feeds.tsx b/apps/app/components/core/feeds.tsx index bd0e4c6bf..cce482da5 100644 --- a/apps/app/components/core/feeds.tsx +++ b/apps/app/components/core/feeds.tsx @@ -1,5 +1,6 @@ import React from "react"; -import Image from "next/image"; + +import Link from "next/link"; // icons import { @@ -22,7 +23,6 @@ import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper" import { addSpaceIfCamelCase } from "helpers/string.helper"; // types import RemirrorRichTextEditor from "components/rich-text-editor"; -import Link from "next/link"; const activityDetails: { [key: string]: { @@ -206,7 +206,7 @@ export const Feeds: React.FC = ({ activities }) => (
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( - {activity.actor_detail.first_name} = ({ activities }) => ( activityDetails[activity.field as keyof typeof activityDetails]?.icon ) : activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( - {activity.actor_detail.first_name} = ({ const router = useRouter(); const { workspaceSlug } = router.query; + const { user } = useUserAuth(); + const editorRef = useRef(null); const { setToastAlert } = useToast(); @@ -97,10 +100,15 @@ export const GptAssistantModal: React.FC = ({ } await aiService - .createGptTask(workspaceSlug as string, projectId as string, { - prompt: content && content !== "" ? content : htmlContent ?? "", - task: formData.task, - }) + .createGptTask( + workspaceSlug as string, + projectId as string, + { + prompt: content && content !== "" ? content : htmlContent ?? "", + task: formData.task, + }, + user + ) .then((res) => { setResponse(res.response_html); setFocus("task"); @@ -190,10 +198,15 @@ export const GptAssistantModal: React.FC = ({ if (block) trackEventServices.trackUseGPTResponseEvent( block, - "USE_GPT_RESPONSE_IN_PAGE_BLOCK" + "USE_GPT_RESPONSE_IN_PAGE_BLOCK", + user ); else if (issue) - trackEventServices.trackUseGPTResponseEvent(issue, "USE_GPT_RESPONSE_IN_ISSUE"); + trackEventServices.trackUseGPTResponseEvent( + issue, + "USE_GPT_RESPONSE_IN_ISSUE", + user + ); }} > Use this response diff --git a/apps/app/components/core/image-picker-popover.tsx b/apps/app/components/core/image-picker-popover.tsx index 8b2edf3fb..380c6f356 100644 --- a/apps/app/components/core/image-picker-popover.tsx +++ b/apps/app/components/core/image-picker-popover.tsx @@ -1,8 +1,5 @@ import React, { useEffect, useState, useRef } from "react"; -// next -import Image from "next/image"; - // swr import useSWR from "swr"; @@ -107,12 +104,7 @@ export const ImagePickerPopover: React.FC = ({ label, value, onChange }) onChange={(e) => setFormData({ ...formData, search: e.target.value })} placeholder="Search for images" /> - setSearchParams(formData.search)} - className="bg-indigo-600" - size="sm" - > + setSearchParams(formData.search)} size="sm"> Search
@@ -123,12 +115,10 @@ export const ImagePickerPopover: React.FC = ({ label, value, onChange }) key={image.id} className="relative col-span-2 aspect-video md:col-span-1" > - {image.alt_description} { setIsOpen(false); onChange(image.urls.regular); diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index cc0672843..bfa6b9f46 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -17,6 +17,7 @@ import { useProjectMyMembership } from "contexts/project-member.context"; // hooks import useToast from "hooks/use-toast"; import useIssuesView from "hooks/use-issues-view"; +import useUserAuth from "hooks/use-user-auth"; // components import { AllLists, AllBoards, FilterList, CalendarView, GanttChartView } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; @@ -89,6 +90,8 @@ export const IssuesView: React.FC = ({ const { memberRole } = useProjectMyMembership(); + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { @@ -220,11 +223,17 @@ export const IssuesView: React.FC = ({ // patch request issuesService - .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { - priority: draggedItem.priority, - state: draggedItem.state, - sort_order: draggedItem.sort_order, - }) + .patchIssue( + workspaceSlug as string, + projectId as string, + draggedItem.id, + { + priority: draggedItem.priority, + state: draggedItem.state, + sort_order: draggedItem.sort_order, + }, + user + ) .then((response) => { const sourceStateBeforeDrag = states.find((state) => state.name === source.droppableId); @@ -232,14 +241,17 @@ export const IssuesView: React.FC = ({ sourceStateBeforeDrag?.group !== "completed" && response?.state_detail?.group === "completed" ) - trackEventServices.trackIssueMarkedAsDoneEvent({ - workspaceSlug, - workspaceId: draggedItem.workspace, - projectName: draggedItem.project_detail.name, - projectIdentifier: draggedItem.project_detail.identifier, - projectId, - issueId: draggedItem.id, - }); + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug, + workspaceId: draggedItem.workspace, + projectName: draggedItem.project_detail.name, + projectIdentifier: draggedItem.project_detail.identifier, + projectId, + issueId: draggedItem.id, + }, + user + ); if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); @@ -419,6 +431,7 @@ export const IssuesView: React.FC = ({ isOpen={createViewModal !== null} handleClose={() => setCreateViewModal(null)} preLoadedData={createViewModal} + user={user} /> = ({ handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueToDelete} + user={user} /> setTransferIssuesModal(false)} @@ -508,6 +522,7 @@ export const IssuesView: React.FC = ({ : null } isCompleted={isCompleted} + user={user} userAuth={memberRole} /> ) : issueView === "kanban" ? ( @@ -528,6 +543,7 @@ export const IssuesView: React.FC = ({ : null } isCompleted={isCompleted} + user={user} userAuth={memberRole} /> ) : issueView === "calendar" ? ( @@ -536,6 +552,7 @@ export const IssuesView: React.FC = ({ handleDeleteIssue={handleDeleteIssue} addIssueToDate={addIssueToDate} isCompleted={isCompleted} + user={user} userAuth={memberRole} /> ) : ( diff --git a/apps/app/components/core/list-view/all-lists.tsx b/apps/app/components/core/list-view/all-lists.tsx index d8fa8b9ee..fd063728a 100644 --- a/apps/app/components/core/list-view/all-lists.tsx +++ b/apps/app/components/core/list-view/all-lists.tsx @@ -3,7 +3,7 @@ import useIssuesView from "hooks/use-issues-view"; // components import { SingleList } from "components/core/list-view/single-list"; // types -import { IIssue, IState, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; // types type Props = { @@ -16,6 +16,7 @@ type Props = { openIssuesListModal?: (() => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -29,6 +30,7 @@ export const AllLists: React.FC = ({ handleDeleteIssue, removeIssue, isCompleted = false, + user, userAuth, }) => { const { groupedByIssues, groupByProperty: selectedGroup, showEmptyGroups } = useIssuesView(); @@ -58,6 +60,7 @@ export const AllLists: React.FC = ({ openIssuesListModal={type !== "issue" ? openIssuesListModal : null} removeIssue={removeIssue} isCompleted={isCompleted} + user={user} userAuth={userAuth} /> ); diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx index 6f6a5ab4f..ea4ebc811 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -36,7 +36,7 @@ import { LayerDiagonalIcon } from "components/icons"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { handleIssuesMutation } from "constants/issue"; // types -import { IIssue, Properties, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -57,6 +57,7 @@ type Props = { removeIssue?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -71,6 +72,7 @@ export const SingleListIssue: React.FC = ({ groupTitle, handleDeleteIssue, isCompleted = false, + user, userAuth, }) => { // context menu @@ -141,7 +143,7 @@ export const SingleListIssue: React.FC = ({ ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData) + .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) .then(() => { if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); @@ -241,6 +243,7 @@ export const SingleListIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="right" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -249,6 +252,7 @@ export const SingleListIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="right" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -256,6 +260,7 @@ export const SingleListIssue: React.FC = ({ )} @@ -284,6 +289,7 @@ export const SingleListIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="right" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -292,6 +298,7 @@ export const SingleListIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="right" + user={user} isNotAllowed={isNotAllowed} /> )} diff --git a/apps/app/components/core/list-view/single-list.tsx b/apps/app/components/core/list-view/single-list.tsx index dd5ffb110..a8c6e4a51 100644 --- a/apps/app/components/core/list-view/single-list.tsx +++ b/apps/app/components/core/list-view/single-list.tsx @@ -19,7 +19,14 @@ import { getPriorityIcon, getStateGroupIcon } from "components/icons"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IIssueLabels, IState, TIssueGroupByOptions, UserAuth } from "types"; +import { + ICurrentUserResponse, + IIssue, + IIssueLabels, + IState, + TIssueGroupByOptions, + UserAuth, +} from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; @@ -39,6 +46,7 @@ type Props = { openIssuesListModal?: (() => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -56,6 +64,7 @@ export const SingleList: React.FC = ({ openIssuesListModal, removeIssue, isCompleted = false, + user, userAuth, }) => { const router = useRouter(); @@ -208,6 +217,7 @@ export const SingleList: React.FC = ({ removeIssue(issue.bridge_id, issue.id); }} isCompleted={isCompleted} + user={user} userAuth={userAuth} /> )) diff --git a/apps/app/components/cycles/active-cycle-details.tsx b/apps/app/components/cycles/active-cycle-details.tsx index 65c2e8b1b..21f37e5a6 100644 --- a/apps/app/components/cycles/active-cycle-details.tsx +++ b/apps/app/components/cycles/active-cycle-details.tsx @@ -1,7 +1,6 @@ import React from "react"; import Link from "next/link"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; @@ -40,25 +39,12 @@ import { } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; // types -import { - CompletedCyclesResponse, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, - ICycle, - IIssue, -} from "types"; +import { ICycle, IIssue } from "types"; // fetch-keys -import { - CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, - CYCLE_DRAFT_LIST, - CYCLE_ISSUES, -} from "constants/fetch-keys"; +import { CURRENT_CYCLE_LIST, CYCLES_LIST, CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; type TSingleStatProps = { cycle: ICycle; - isCompleted?: boolean; }; const stateGroups = [ @@ -89,7 +75,7 @@ const stateGroups = [ }, ]; -export const ActiveCycleDetails: React.FC = ({ cycle, isCompleted = false }) => { +export const ActiveCycleDetails: React.FC = ({ cycle }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -111,51 +97,18 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple const handleAddToFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - } + mutate( + CURRENT_CYCLE_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLES_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -180,51 +133,18 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple const handleRemoveFromFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - } + mutate( + CURRENT_CYCLE_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLES_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -244,17 +164,20 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple }); }; - const { data: issues } = useSWR( - workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null, + const { data: issues } = useSWR( + workspaceSlug && projectId && cycle.id + ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "high" }) + : null, workspaceSlug && projectId && cycle.id ? () => - cyclesService.getCycleIssues( + cyclesService.getCycleIssuesWithParams( workspaceSlug as string, projectId as string, - cycle.id as string + cycle.id, + { priority: "high" } ) : null - ); + ) as { data: IIssue[] }; const progressIndicatorData = stateGroups.map((group, index) => ({ id: index, @@ -379,7 +302,7 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - = ({ issues }) => { -
- + >; - setSelectedCycle: React.Dispatch>; - type: "current" | "upcoming" | "draft"; -}; - -export const AllCyclesBoard: React.FC = ({ - cycles, - setCreateUpdateCycleModal, - setSelectedCycle, - type, -}) => { - const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); - - const handleDeleteCycle = (cycle: ICycle) => { - setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); - setCycleDeleteModal(true); - }; - - const handleEditCycle = (cycle: ICycle) => { - setSelectedCycle({ ...cycle, actionType: "edit" }); - setCreateUpdateCycleModal(true); - }; - - return ( - <> - - {cycles ? ( - cycles.length > 0 ? ( -
- {cycles.map((cycle) => ( - handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - /> - ))} -
- ) : type === "current" ? ( -
-

No cycle is present.

-
- ) : ( - - ) - ) : ( - - - - )} - - ); -}; diff --git a/apps/app/components/cycles/all-cycles-list.tsx b/apps/app/components/cycles/all-cycles-list.tsx deleted file mode 100644 index 502e13e8e..000000000 --- a/apps/app/components/cycles/all-cycles-list.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useState } from "react"; - -// components -import { DeleteCycleModal, SingleCycleList } from "components/cycles"; -import { EmptyState, Loader } from "components/ui"; -// image -import emptyCycle from "public/empty-state/empty-cycle.svg"; -// icon -import { XMarkIcon } from "@heroicons/react/24/outline"; -// types -import { ICycle, SelectCycleType } from "types"; - -type TCycleStatsViewProps = { - cycles: ICycle[] | undefined; - setCreateUpdateCycleModal: React.Dispatch>; - setSelectedCycle: React.Dispatch>; - type: "current" | "upcoming" | "draft"; -}; - -export const AllCyclesList: React.FC = ({ - cycles, - setCreateUpdateCycleModal, - setSelectedCycle, - type, -}) => { - const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); - - const handleDeleteCycle = (cycle: ICycle) => { - setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); - setCycleDeleteModal(true); - }; - - const handleEditCycle = (cycle: ICycle) => { - setSelectedCycle({ ...cycle, actionType: "edit" }); - setCreateUpdateCycleModal(true); - }; - - return ( - <> - - {cycles ? ( - cycles.length > 0 ? ( -
- {cycles.map((cycle) => ( -
-
- handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - /> -
-
- ))} -
- ) : type === "current" ? ( -
-

No cycle is present.

-
- ) : ( - - ) - ) : ( - - - - )} - - ); -}; diff --git a/apps/app/components/cycles/completed-cycles.tsx b/apps/app/components/cycles/completed-cycles.tsx deleted file mode 100644 index 36ef691f5..000000000 --- a/apps/app/components/cycles/completed-cycles.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useState } from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// services -import cyclesService from "services/cycles.service"; -// components -import { DeleteCycleModal, SingleCycleCard, SingleCycleList } from "components/cycles"; -// icons -import { ExclamationIcon } from "components/icons"; -// types -import { ICycle, SelectCycleType } from "types"; -// fetch-keys -import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys"; -import { EmptyState, Loader } from "components/ui"; -// image -import emptyCycle from "public/empty-state/empty-cycle.svg"; - -export interface CompletedCyclesListProps { - cycleView: string; - setCreateUpdateCycleModal: React.Dispatch>; - setSelectedCycle: React.Dispatch>; -} - -export const CompletedCycles: React.FC = ({ - cycleView, - setCreateUpdateCycleModal, - setSelectedCycle, -}) => { - const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { data: completedCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => cyclesService.getCompletedCycles(workspaceSlug as string, projectId as string) - : null - ); - - const handleDeleteCycle = (cycle: ICycle) => { - setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); - setCycleDeleteModal(true); - }; - - const handleEditCycle = (cycle: ICycle) => { - setSelectedCycle({ ...cycle, actionType: "edit" }); - setCreateUpdateCycleModal(true); - }; - - return ( - <> - - {completedCycles ? ( - completedCycles.completed_cycles.length > 0 ? ( -
-
- - Completed cycles are not editable. -
- {cycleView === "list" && ( -
- {completedCycles.completed_cycles.map((cycle) => ( -
-
- handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - isCompleted - /> -
-
- ))} -
- )} - {cycleView === "board" && ( -
- {completedCycles.completed_cycles.map((cycle) => ( - handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - isCompleted - /> - ))} -
- )} -
- ) : ( - - ) - ) : ( - - - - - - )} - - ); -}; diff --git a/apps/app/components/cycles/cycles-list-gantt-chart.tsx b/apps/app/components/cycles/cycles-list-gantt-chart.tsx index 9c8e922d2..056872c33 100644 --- a/apps/app/components/cycles/cycles-list-gantt-chart.tsx +++ b/apps/app/components/cycles/cycles-list-gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // types import { ICycle } from "types"; @@ -31,9 +33,11 @@ export const CyclesListGanttChartView: FC = ({ cycles }) => {
-
- {data?.name} -
+ +
+ {data?.name} +
+
); diff --git a/apps/app/components/cycles/cycles-list/all-cycles-list.tsx b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx new file mode 100644 index 000000000..7ebd92a50 --- /dev/null +++ b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx @@ -0,0 +1,29 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// components +import { CyclesView } from "components/cycles"; +// fetch-keys +import { CYCLES_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: string | null; +}; + +export const AllCyclesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: allCyclesList } = useSWR( + workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null, + workspaceSlug && projectId + ? () => + cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "all") + : null + ); + + return ; +}; diff --git a/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx new file mode 100644 index 000000000..79a427d95 --- /dev/null +++ b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx @@ -0,0 +1,33 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// components +import { CyclesView } from "components/cycles"; +// fetch-keys +import { COMPLETED_CYCLES_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: string | null; +}; + +export const CompletedCyclesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: completedCyclesList } = useSWR( + workspaceSlug && projectId ? COMPLETED_CYCLES_LIST(projectId.toString()) : null, + workspaceSlug && projectId + ? () => + cyclesService.getCyclesWithParams( + workspaceSlug.toString(), + projectId.toString(), + "completed" + ) + : null + ); + + return ; +}; diff --git a/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx new file mode 100644 index 000000000..fd2dccc93 --- /dev/null +++ b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx @@ -0,0 +1,29 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// components +import { CyclesView } from "components/cycles"; +// fetch-keys +import { DRAFT_CYCLES_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: string | null; +}; + +export const DraftCyclesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: draftCyclesList } = useSWR( + workspaceSlug && projectId ? DRAFT_CYCLES_LIST(projectId.toString()) : null, + workspaceSlug && projectId + ? () => + cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "draft") + : null + ); + + return ; +}; diff --git a/apps/app/components/cycles/cycles-list/index.ts b/apps/app/components/cycles/cycles-list/index.ts new file mode 100644 index 000000000..b4b4fdfb5 --- /dev/null +++ b/apps/app/components/cycles/cycles-list/index.ts @@ -0,0 +1,4 @@ +export * from "./all-cycles-list"; +export * from "./completed-cycles-list"; +export * from "./draft-cycles-list"; +export * from "./upcoming-cycles-list"; diff --git a/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx new file mode 100644 index 000000000..140727cb8 --- /dev/null +++ b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx @@ -0,0 +1,33 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// components +import { CyclesView } from "components/cycles"; +// fetch-keys +import { UPCOMING_CYCLES_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: string | null; +}; + +export const UpcomingCyclesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: upcomingCyclesList } = useSWR( + workspaceSlug && projectId ? UPCOMING_CYCLES_LIST(projectId.toString()) : null, + workspaceSlug && projectId + ? () => + cyclesService.getCyclesWithParams( + workspaceSlug.toString(), + projectId.toString(), + "upcoming" + ) + : null + ); + + return ; +}; diff --git a/apps/app/components/cycles/cycles-view.tsx b/apps/app/components/cycles/cycles-view.tsx index aedd82ba3..261e81018 100644 --- a/apps/app/components/cycles/cycles-view.tsx +++ b/apps/app/components/cycles/cycles-view.tsx @@ -1,249 +1,233 @@ -import React, { useEffect } from "react"; -import dynamic from "next/dynamic"; -// headless ui -import { Tab } from "@headlessui/react"; +import React, { useState } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// services +import cyclesService from "services/cycles.service"; // hooks -import useLocalStorage from "hooks/use-local-storage"; +import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // components import { - ActiveCycleDetails, - CompletedCyclesListProps, - AllCyclesBoard, - AllCyclesList, + CreateUpdateCycleModal, CyclesListGanttChartView, + DeleteCycleModal, + SingleCycleCard, + SingleCycleList, } from "components/cycles"; // ui import { EmptyState, Loader } from "components/ui"; -// icons -import { ChartBarIcon, ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +// images import emptyCycle from "public/empty-state/empty-cycle.svg"; +// helpers +import { getDateRangeStatus } from "helpers/date-time.helper"; // types +import { ICycle } from "types"; +// fetch-keys import { - SelectCycleType, - ICycle, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, -} from "types"; + COMPLETED_CYCLES_LIST, + CURRENT_CYCLE_LIST, + CYCLES_LIST, + DRAFT_CYCLES_LIST, + UPCOMING_CYCLES_LIST, +} from "constants/fetch-keys"; type Props = { - setSelectedCycle: React.Dispatch>; - setCreateUpdateCycleModal: React.Dispatch>; - cyclesCompleteList: ICycle[] | undefined; - currentAndUpcomingCycles: CurrentAndUpcomingCyclesResponse | undefined; - draftCycles: DraftCyclesResponse | undefined; + cycles: ICycle[] | undefined; + viewType: string | null; }; -export const CyclesView: React.FC = ({ - setSelectedCycle, - setCreateUpdateCycleModal, - cyclesCompleteList, - currentAndUpcomingCycles, - draftCycles, -}) => { - const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All"); - const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycleView", "list"); +export const CyclesView: React.FC = ({ cycles, viewType }) => { + const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false); + const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState(null); - const currentTabValue = (tab: string | null) => { - switch (tab) { - case "All": - return 0; - case "Active": - return 1; - case "Upcoming": - return 2; - case "Completed": - return 3; - case "Drafts": - return 4; - default: - return 0; - } + const [deleteCycleModal, setDeleteCycleModal] = useState(false); + const [selectedCycleToDelete, setSelectedCycleToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); + + const handleEditCycle = (cycle: ICycle) => { + setSelectedCycleToUpdate(cycle); + setCreateUpdateCycleModal(true); }; - const CompletedCycles = dynamic( - () => import("components/cycles").then((a) => a.CompletedCycles), - { - ssr: false, - loading: () => ( - - - - ), - } - ); + const handleDeleteCycle = (cycle: ICycle) => { + setSelectedCycleToDelete(cycle); + setDeleteCycleModal(true); + }; + + const handleAddToFavorites = (cycle: ICycle) => { + if (!workspaceSlug || !projectId) return; + + const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + + const fetchKey = + cycleStatus === "current" + ? CURRENT_CYCLE_LIST(projectId as string) + : cycleStatus === "upcoming" + ? UPCOMING_CYCLES_LIST(projectId as string) + : cycleStatus === "completed" + ? COMPLETED_CYCLES_LIST(projectId as string) + : DRAFT_CYCLES_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + + mutate( + CYCLES_LIST(projectId as string), + (prevData: any) => + (prevData ?? []).map((c: any) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + + cyclesService + .addCycleToFavorites(workspaceSlug as string, projectId as string, { + cycle: cycle.id, + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", + }); + }); + }; + + const handleRemoveFromFavorites = (cycle: ICycle) => { + if (!workspaceSlug || !projectId) return; + + const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + + const fetchKey = + cycleStatus === "current" + ? CURRENT_CYCLE_LIST(projectId as string) + : cycleStatus === "upcoming" + ? UPCOMING_CYCLES_LIST(projectId as string) + : cycleStatus === "completed" + ? COMPLETED_CYCLES_LIST(projectId as string) + : DRAFT_CYCLES_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + + mutate( + CYCLES_LIST(projectId as string), + (prevData: any) => + (prevData ?? []).map((c: any) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + + cyclesService + .removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the cycle from favorites. Please try again.", + }); + }); + }; return ( <> -
-

Cycles

-
- - - -
-
- { - switch (i) { - case 0: - return setCycleTab("All"); - case 1: - return setCycleTab("Active"); - case 2: - return setCycleTab("Upcoming"); - case 3: - return setCycleTab("Completed"); - case 4: - return setCycleTab("Drafts"); - default: - return setCycleTab("All"); - } - }} - > -
- - {["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => { - if ( - cyclesView === "gantt_chart" && - (tab === "Active" || tab === "Drafts" || tab === "Completed") - ) - return null; - - return ( - - `rounded-3xl border px-6 py-1 outline-none ${ - selected - ? "border-brand-accent bg-brand-accent text-white font-medium" - : "border-brand-base bg-brand-base hover:bg-brand-surface-2" - }` - } - > - {tab} - - ); - })} - -
- - - {cyclesView === "list" && ( - - )} - {cyclesView === "board" && ( - - )} - {cyclesView === "gantt_chart" && ( - - )} - - {cyclesView !== "gantt_chart" && ( - - {currentAndUpcomingCycles?.current_cycle?.[0] ? ( - - ) : ( - setCreateUpdateCycleModal(false)} + data={selectedCycleToUpdate} + user={user} + /> + + {cycles ? ( + cycles.length > 0 ? ( + viewType === "list" ? ( +
+ {cycles.map((cycle) => ( +
+
+ handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + handleAddToFavorites={() => handleAddToFavorites(cycle)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)} + /> +
+
+ ))} +
+ ) : viewType === "board" ? ( +
+ {cycles.map((cycle) => ( + handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + handleAddToFavorites={() => handleAddToFavorites(cycle)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)} /> - )} - - )} - - {cyclesView === "list" && ( - - )} - {cyclesView === "board" && ( - - )} - {cyclesView === "gantt_chart" && ( - - )} - - - - - {cyclesView !== "gantt_chart" && ( - - {cyclesView === "list" && ( - - )} - {cyclesView === "board" && ( - - )} - - )} - - + ))} +
+ ) : ( + + ) + ) : ( + + ) + ) : viewType === "list" ? ( + + + + + + ) : viewType === "board" ? ( + + + + + + ) : ( + + + + )} ); }; diff --git a/apps/app/components/cycles/delete-cycle-modal.tsx b/apps/app/components/cycles/delete-cycle-modal.tsx index 136a9b847..3f2de2913 100644 --- a/apps/app/components/cycles/delete-cycle-modal.tsx +++ b/apps/app/components/cycles/delete-cycle-modal.tsx @@ -14,24 +14,20 @@ import { DangerButton, SecondaryButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import type { - CompletedCyclesResponse, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, - ICycle, -} from "types"; +import type { ICurrentUserResponse, ICycle } from "types"; type TConfirmCycleDeletionProps = { isOpen: boolean; setIsOpen: React.Dispatch>; - data?: ICycle; + data?: ICycle | null; + user: ICurrentUserResponse | undefined; }; // fetch-keys import { - CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, - CYCLE_DRAFT_LIST, - CYCLE_LIST, + COMPLETED_CYCLES_LIST, + CURRENT_CYCLE_LIST, + CYCLES_LIST, + DRAFT_CYCLES_LIST, + UPCOMING_CYCLES_LIST, } from "constants/fetch-keys"; import { getDateRangeStatus } from "helpers/date-time.helper"; @@ -39,6 +35,7 @@ export const DeleteCycleModal: React.FC = ({ isOpen, setIsOpen, data, + user, }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -58,65 +55,30 @@ export const DeleteCycleModal: React.FC = ({ setIsDeleteLoading(true); await cycleService - .deleteCycle(workspaceSlug as string, data.project, data.id) + .deleteCycle(workspaceSlug as string, data.project, data.id, user) .then(() => { - switch (getDateRangeStatus(data.start_date, data.end_date)) { - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => { - if (!prevData) return; + const cycleType = getDateRangeStatus(data.start_date, data.end_date); + const fetchKey = + cycleType === "current" + ? CURRENT_CYCLE_LIST(projectId as string) + : cycleType === "upcoming" + ? UPCOMING_CYCLES_LIST(projectId as string) + : cycleType === "completed" + ? COMPLETED_CYCLES_LIST(projectId as string) + : DRAFT_CYCLES_LIST(projectId as string); - return { - completed_cycles: prevData.completed_cycles?.filter( - (cycle) => cycle.id !== data?.id - ), - }; - }, - false - ); - break; - case "current": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => { - if (!prevData) return; - return { - current_cycle: prevData.current_cycle?.filter((c) => c.id !== data?.id), - upcoming_cycle: prevData.upcoming_cycle, - }; - }, - false - ); - break; - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => { - if (!prevData) return; + mutate( + fetchKey, + (prevData) => { + if (!prevData) return; + + return prevData.filter((cycle) => cycle.id !== data?.id); + }, + false + ); - return { - current_cycle: prevData.current_cycle, - upcoming_cycle: prevData.upcoming_cycle?.filter((c) => c.id !== data?.id), - }; - }, - false - ); - break; - default: - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => { - if (!prevData) return; - return { - draft_cycles: prevData.draft_cycles?.filter((cycle) => cycle.id !== data?.id), - }; - }, - false - ); - } mutate( - CYCLE_DETAILS(projectId as string), + CYCLES_LIST(projectId as string), (prevData: any) => { if (!prevData) return; return prevData.filter((cycle: any) => cycle.id !== data?.id); diff --git a/apps/app/components/cycles/empty-cycle.tsx b/apps/app/components/cycles/empty-cycle.tsx deleted file mode 100644 index af2f12a11..000000000 --- a/apps/app/components/cycles/empty-cycle.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import { LinearProgressIndicator } from "components/ui"; - -export const EmptyCycle = () => { - const emptyCycleData = [ - { - id: 1, - name: "backlog", - value: 20, - color: "#DEE2E6", - }, - { - id: 2, - name: "unstarted", - value: 14, - color: "#26B5CE", - }, - { - id: 3, - name: "started", - value: 27, - color: "#F7AE59", - }, - { - id: 4, - name: "cancelled", - value: 15, - color: "#D687FF", - }, - { - id: 5, - name: "completed", - value: 14, - color: "#09A953", - }, - ]; - return ( -
-
-
-
- Cycle Name -
- - -
-
- -
- -
-
- -
-
- Cycle Name -
- - -
-
- -
- -
-
-
- -
-

Create New Cycle

-

- Sprint more effectively with Cycles by confining your project
to a fixed amount of - time. Create new cycle now. -

-
-
- ); -}; diff --git a/apps/app/components/cycles/form.tsx b/apps/app/components/cycles/form.tsx index a0bd781ce..5c0b2e080 100644 --- a/apps/app/components/cycles/form.tsx +++ b/apps/app/components/cycles/form.tsx @@ -12,7 +12,7 @@ type Props = { handleFormSubmit: (values: Partial) => Promise; handleClose: () => void; status: boolean; - data?: ICycle; + data?: ICycle | null; }; const defaultValues: Partial = { @@ -28,7 +28,6 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat formState: { errors, isSubmitting }, handleSubmit, control, - watch, reset, } = useForm({ defaultValues, diff --git a/apps/app/components/cycles/gantt-chart.tsx b/apps/app/components/cycles/gantt-chart.tsx index 44abc392b..42ecad448 100644 --- a/apps/app/components/cycles/gantt-chart.tsx +++ b/apps/app/components/cycles/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view"; @@ -38,9 +40,23 @@ export const CycleIssuesGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )} ); @@ -59,10 +75,20 @@ export const CycleIssuesGanttChartView: FC = ({}) => { const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/cycles/index.ts b/apps/app/components/cycles/index.ts index 3151529c5..40355d574 100644 --- a/apps/app/components/cycles/index.ts +++ b/apps/app/components/cycles/index.ts @@ -1,18 +1,15 @@ +export * from "./cycles-list"; export * from "./active-cycle-details"; -export * from "./cycles-view"; -export * from "./completed-cycles"; +export * from "./active-cycle-stats"; export * from "./cycles-list-gantt-chart"; -export * from "./all-cycles-board"; -export * from "./all-cycles-list"; +export * from "./cycles-view"; export * from "./delete-cycle-modal"; export * from "./form"; export * from "./gantt-chart"; export * from "./modal"; export * from "./select"; export * from "./sidebar"; -export * from "./single-cycle-list"; export * from "./single-cycle-card"; -export * from "./empty-cycle"; +export * from "./single-cycle-list"; export * from "./transfer-issues-modal"; export * from "./transfer-issues"; -export * from "./active-cycle-stats"; diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index b3baacd65..3c1c16506 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -15,26 +15,29 @@ import { CycleForm } from "components/cycles"; // helper import { getDateRangeStatus, isDateGreaterThanToday } from "helpers/date-time.helper"; // types -import type { ICycle } from "types"; +import type { ICurrentUserResponse, ICycle } from "types"; // fetch keys import { - CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, - CYCLE_DRAFT_LIST, - CYCLE_INCOMPLETE_LIST, + COMPLETED_CYCLES_LIST, + CURRENT_CYCLE_LIST, + CYCLES_LIST, + DRAFT_CYCLES_LIST, + INCOMPLETE_CYCLES_LIST, + UPCOMING_CYCLES_LIST, } from "constants/fetch-keys"; type CycleModalProps = { isOpen: boolean; handleClose: () => void; - data?: ICycle; + data?: ICycle | null; + user: ICurrentUserResponse | undefined; }; export const CreateUpdateCycleModal: React.FC = ({ isOpen, handleClose, data, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -42,24 +45,26 @@ export const CreateUpdateCycleModal: React.FC = ({ const { setToastAlert } = useToast(); const createCycle = async (payload: Partial) => { + if (!workspaceSlug || !projectId) return; + await cycleService - .createCycle(workspaceSlug as string, projectId as string, payload) + .createCycle(workspaceSlug.toString(), projectId.toString(), payload, user) .then((res) => { switch (getDateRangeStatus(res.start_date, res.end_date)) { case "completed": - mutate(CYCLE_COMPLETE_LIST(projectId as string)); + mutate(COMPLETED_CYCLES_LIST(projectId.toString())); break; case "current": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CURRENT_CYCLE_LIST(projectId.toString())); break; case "upcoming": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(UPCOMING_CYCLES_LIST(projectId.toString())); break; default: - mutate(CYCLE_DRAFT_LIST(projectId as string)); + mutate(DRAFT_CYCLES_LIST(projectId.toString())); } - mutate(CYCLE_INCOMPLETE_LIST(projectId as string)); - mutate(CYCLE_DETAILS(projectId as string)); + mutate(INCOMPLETE_CYCLES_LIST(projectId.toString())); + mutate(CYCLES_LIST(projectId.toString())); handleClose(); setToastAlert({ @@ -68,7 +73,7 @@ export const CreateUpdateCycleModal: React.FC = ({ message: "Cycle created successfully.", }); }) - .catch((err) => { + .catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -78,39 +83,41 @@ export const CreateUpdateCycleModal: React.FC = ({ }; const updateCycle = async (cycleId: string, payload: Partial) => { + if (!workspaceSlug || !projectId) return; + await cycleService - .updateCycle(workspaceSlug as string, projectId as string, cycleId, payload) + .updateCycle(workspaceSlug.toString(), projectId.toString(), cycleId, payload, user) .then((res) => { switch (getDateRangeStatus(data?.start_date, data?.end_date)) { case "completed": - mutate(CYCLE_COMPLETE_LIST(projectId as string)); + mutate(COMPLETED_CYCLES_LIST(projectId.toString())); break; case "current": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CURRENT_CYCLE_LIST(projectId.toString())); break; case "upcoming": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(UPCOMING_CYCLES_LIST(projectId.toString())); break; default: - mutate(CYCLE_DRAFT_LIST(projectId as string)); + mutate(DRAFT_CYCLES_LIST(projectId.toString())); } - mutate(CYCLE_DETAILS(projectId as string)); + mutate(CYCLES_LIST(projectId.toString())); if ( getDateRangeStatus(data?.start_date, data?.end_date) != getDateRangeStatus(res.start_date, res.end_date) ) { switch (getDateRangeStatus(res.start_date, res.end_date)) { case "completed": - mutate(CYCLE_COMPLETE_LIST(projectId as string)); + mutate(COMPLETED_CYCLES_LIST(projectId.toString())); break; case "current": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CURRENT_CYCLE_LIST(projectId.toString())); break; case "upcoming": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(UPCOMING_CYCLES_LIST(projectId.toString())); break; default: - mutate(CYCLE_DRAFT_LIST(projectId as string)); + mutate(DRAFT_CYCLES_LIST(projectId.toString())); } } diff --git a/apps/app/components/cycles/select.tsx b/apps/app/components/cycles/select.tsx index 971c60ddd..2fdda69b5 100644 --- a/apps/app/components/cycles/select.tsx +++ b/apps/app/components/cycles/select.tsx @@ -4,6 +4,7 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +import useUserAuth from "hooks/use-user-auth"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // icons @@ -14,7 +15,7 @@ import cycleServices from "services/cycles.service"; // components import { CreateUpdateCycleModal } from "components/cycles"; // fetch-keys -import { CYCLE_LIST } from "constants/fetch-keys"; +import { CYCLES_LIST } from "constants/fetch-keys"; export type IssueCycleSelectProps = { projectId: string; @@ -35,10 +36,12 @@ export const CycleSelect: React.FC = ({ const router = useRouter(); const { workspaceSlug } = router.query; + const { user } = useUserAuth(); + const { data: cycles } = useSWR( - workspaceSlug && projectId ? CYCLE_LIST(projectId) : null, + workspaceSlug && projectId ? CYCLES_LIST(projectId) : null, workspaceSlug && projectId - ? () => cycleServices.getCycles(workspaceSlug as string, projectId) + ? () => cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, "all") : null ); @@ -54,7 +57,11 @@ export const CycleSelect: React.FC = ({ return ( <> - + {({ open }) => ( <> diff --git a/apps/app/components/cycles/sidebar.tsx b/apps/app/components/cycles/sidebar.tsx index 494383fc4..03747fc3b 100644 --- a/apps/app/components/cycles/sidebar.tsx +++ b/apps/app/components/cycles/sidebar.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import Image from "next/image"; import useSWR, { mutate } from "swr"; @@ -39,7 +38,7 @@ import { renderShortDate, } from "helpers/date-time.helper"; // types -import { ICycle, IIssue } from "types"; +import { ICurrentUserResponse, ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_DETAILS, CYCLE_ISSUES } from "constants/fetch-keys"; @@ -48,6 +47,7 @@ type Props = { isOpen: boolean; cycleStatus: string; isCompleted: boolean; + user: ICurrentUserResponse | undefined; }; export const CycleDetailsSidebar: React.FC = ({ @@ -55,6 +55,7 @@ export const CycleDetailsSidebar: React.FC = ({ isOpen, cycleStatus, isCompleted, + user, }) => { const [cycleDeleteModal, setCycleDeleteModal] = useState(false); @@ -94,7 +95,7 @@ export const CycleDetailsSidebar: React.FC = ({ ); cyclesService - .patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data) + .patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data, user) .then(() => mutate(CYCLE_DETAILS(cycleId as string))) .catch((e) => console.log(e)); }; @@ -294,7 +295,12 @@ export const CycleDetailsSidebar: React.FC = ({ return ( <> - +
= ({
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - void; handleDeleteCycle: () => void; + handleAddToFavorites: () => void; + handleRemoveFromFavorites: () => void; isCompleted?: boolean; }; @@ -94,6 +80,8 @@ export const SingleCycleCard: React.FC = ({ cycle, handleEditCycle, handleDeleteCycle, + handleAddToFavorites, + handleRemoveFromFavorites, isCompleted = false, }) => { const router = useRouter(); @@ -105,142 +93,6 @@ export const SingleCycleCard: React.FC = ({ const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); - const handleAddToFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; - - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - } - mutate( - CYCLE_DETAILS(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - cyclesService - .addCycleToFavorites(workspaceSlug as string, projectId as string, { - cycle: cycle.id, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); - }; - - const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; - - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - } - mutate( - CYCLE_DETAILS(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); - - cyclesService - .removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the cycle from favorites. Please try again.", - }); - }); - }; - const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; @@ -393,7 +245,7 @@ export const SingleCycleCard: React.FC = ({
Creator:
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - void; handleDeleteCycle: () => void; + handleAddToFavorites: () => void; + handleRemoveFromFavorites: () => void; isCompleted?: boolean; }; @@ -128,6 +112,8 @@ export const SingleCycleList: React.FC = ({ cycle, handleEditCycle, handleDeleteCycle, + handleAddToFavorites, + handleRemoveFromFavorites, isCompleted = false, }) => { const router = useRouter(); @@ -139,142 +125,6 @@ export const SingleCycleList: React.FC = ({ const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); - const handleAddToFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; - - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - } - mutate( - CYCLE_DETAILS(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - cyclesService - .addCycleToFavorites(workspaceSlug as string, projectId as string, { - cycle: cycle.id, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); - }; - - const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; - - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - } - mutate( - CYCLE_DETAILS(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); - - cyclesService - .removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the cycle from favorites. Please try again.", - }); - }); - }; - const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; @@ -302,7 +152,7 @@ export const SingleCycleList: React.FC = ({ return (
-
+
@@ -394,7 +244,7 @@ export const SingleCycleList: React.FC = ({
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - = ({ isOpen, handleClose }) => }; const { data: incompleteCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_INCOMPLETE_LIST(projectId as string) : null, + workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cyclesService.getIncompleteCycles(workspaceSlug as string, projectId as string) + ? () => + cyclesService.getCyclesWithParams( + workspaceSlug as string, + projectId as string, + "incomplete" + ) : null ); diff --git a/apps/app/components/estimates/create-update-estimate-modal.tsx b/apps/app/components/estimates/create-update-estimate-modal.tsx index 3b9660e9d..68cbcaaaf 100644 --- a/apps/app/components/estimates/create-update-estimate-modal.tsx +++ b/apps/app/components/estimates/create-update-estimate-modal.tsx @@ -17,7 +17,7 @@ import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; // helpers import { checkDuplicates } from "helpers/array.helper"; // types -import { IEstimate, IEstimateFormData } from "types"; +import { ICurrentUserResponse, IEstimate, IEstimateFormData } from "types"; // fetch-keys import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys"; @@ -25,6 +25,7 @@ type Props = { isOpen: boolean; handleClose: () => void; data?: IEstimate; + user: ICurrentUserResponse | undefined; }; type FormValues = { @@ -49,7 +50,7 @@ const defaultValues: Partial = { value6: "", }; -export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, isOpen }) => { +export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, isOpen, user }) => { const { register, formState: { isSubmitting }, @@ -73,7 +74,7 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, if (!workspaceSlug || !projectId) return; await estimatesService - .createEstimate(workspaceSlug as string, projectId as string, payload) + .createEstimate(workspaceSlug as string, projectId as string, payload, user) .then(() => { mutate(ESTIMATES_LIST(projectId as string)); onClose(); @@ -118,7 +119,13 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, ); await estimatesService - .patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload) + .patchEstimate( + workspaceSlug as string, + projectId as string, + data?.id as string, + payload, + user + ) .then(() => { mutate(ESTIMATES_LIST(projectId.toString())); mutate(ESTIMATE_DETAILS(data.id)); diff --git a/apps/app/components/estimates/single-estimate.tsx b/apps/app/components/estimates/single-estimate.tsx index 43dcc45b7..5e331c139 100644 --- a/apps/app/components/estimates/single-estimate.tsx +++ b/apps/app/components/estimates/single-estimate.tsx @@ -16,15 +16,17 @@ import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { IEstimate } from "types"; +import { ICurrentUserResponse, IEstimate } from "types"; type Props = { + user: ICurrentUserResponse | undefined; estimate: IEstimate; editEstimate: (estimate: IEstimate) => void; handleEstimateDelete: (estimateId: string) => void; }; export const SingleEstimate: React.FC = ({ + user, estimate, editEstimate, handleEstimateDelete, @@ -52,7 +54,7 @@ export const SingleEstimate: React.FC = ({ }, false); await projectService - .updateProject(workspaceSlug as string, projectId as string, payload) + .updateProject(workspaceSlug as string, projectId as string, payload, user) .catch(() => { setToastAlert({ type: "error", diff --git a/apps/app/components/gantt-chart/blocks/index.tsx b/apps/app/components/gantt-chart/blocks/index.tsx index f2d44b294..d5eadf2a0 100644 --- a/apps/app/components/gantt-chart/blocks/index.tsx +++ b/apps/app/components/gantt-chart/blocks/index.tsx @@ -49,7 +49,10 @@ export const GanttChartBlocks: FC<{ width: `${block?.position?.width}px`, }} > - {blockRender({ ...block?.data })} + {blockRender({ + ...block?.data, + infoToggle: block?.infoToggle ? true : false, + })}
diff --git a/apps/app/components/integration/delete-import-modal.tsx b/apps/app/components/integration/delete-import-modal.tsx index 8c5ab3803..cd0b12a2a 100644 --- a/apps/app/components/integration/delete-import-modal.tsx +++ b/apps/app/components/integration/delete-import-modal.tsx @@ -15,7 +15,7 @@ import { DangerButton, Input, SecondaryButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import { IImporterService } from "types"; +import { ICurrentUserResponse, IImporterService } from "types"; // fetch-keys import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; handleClose: () => void; data: IImporterService | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data }) => { +export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data, user }) => { const [deleteLoading, setDeleteLoading] = useState(false); const [confirmDeleteImport, setConfirmDeleteImport] = useState(false); @@ -45,7 +46,7 @@ export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data } false ); - IntegrationService.deleteImporterService(workspaceSlug as string, data.service, data.id) + IntegrationService.deleteImporterService(workspaceSlug as string, data.service, data.id, user) .catch(() => setToastAlert({ type: "error", diff --git a/apps/app/components/integration/github/root.tsx b/apps/app/components/integration/github/root.tsx index b8bfb346b..96ce845c4 100644 --- a/apps/app/components/integration/github/root.tsx +++ b/apps/app/components/integration/github/root.tsx @@ -27,7 +27,7 @@ import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline"; // images import GithubLogo from "public/services/github.png"; // types -import { IGithubRepoCollaborator, IGithubServiceImportFormData } from "types"; +import { ICurrentUserResponse, IGithubRepoCollaborator, IGithubServiceImportFormData } from "types"; // fetch-keys import { APP_INTEGRATIONS, @@ -89,7 +89,11 @@ const integrationWorkflowData = [ }, ]; -export const GithubImporterRoot = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const GithubImporterRoot: React.FC = ({ user }) => { const [currentStep, setCurrentStep] = useState({ state: "import-configure", }); @@ -157,7 +161,7 @@ export const GithubImporterRoot = () => { project_id: formData.project, }; - await GithubIntegrationService.createGithubServiceImport(workspaceSlug as string, payload) + await GithubIntegrationService.createGithubServiceImport(workspaceSlug as string, payload, user) .then(() => { router.push(`/${workspaceSlug}/settings/import-export`); mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)); diff --git a/apps/app/components/integration/github/single-user-select.tsx b/apps/app/components/integration/github/single-user-select.tsx index 53fbce476..b94fc1dbd 100644 --- a/apps/app/components/integration/github/single-user-select.tsx +++ b/apps/app/components/integration/github/single-user-select.tsx @@ -1,4 +1,3 @@ -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -66,11 +65,9 @@ export const SingleUserSelect: React.FC = ({ collaborator, index, users,
- {`${collaborator.login}
diff --git a/apps/app/components/integration/guide.tsx b/apps/app/components/integration/guide.tsx index 06f13b752..2db8ef010 100644 --- a/apps/app/components/integration/guide.tsx +++ b/apps/app/components/integration/guide.tsx @@ -6,6 +6,8 @@ import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // services import IntegrationService from "services/integration"; // components @@ -35,6 +37,8 @@ const IntegrationGuide = () => { const router = useRouter(); const { workspaceSlug, provider } = router.query; + const { user } = useUserAuth(); + const { data: importerServices } = useSWR( workspaceSlug ? IMPORTER_SERVICES_LIST(workspaceSlug as string) : null, workspaceSlug ? () => IntegrationService.getImporterServicesList(workspaceSlug as string) : null @@ -51,6 +55,7 @@ const IntegrationGuide = () => { isOpen={deleteImportModal} handleClose={() => setDeleteImportModal(false)} data={importToDelete} + user={user} />
{!provider && ( @@ -156,8 +161,8 @@ const IntegrationGuide = () => { )} - {provider && provider === "github" && } - {provider && provider === "jira" && } + {provider && provider === "github" && } + {provider && provider === "jira" && }
); diff --git a/apps/app/components/integration/jira/root.tsx b/apps/app/components/integration/jira/root.tsx index 6ec39a36a..b1098c9cb 100644 --- a/apps/app/components/integration/jira/root.tsx +++ b/apps/app/components/integration/jira/root.tsx @@ -35,7 +35,7 @@ import { import JiraLogo from "public/services/jira.png"; -import { IJiraImporterForm } from "types"; +import { ICurrentUserResponse, IJiraImporterForm } from "types"; const integrationWorkflowData: Array<{ title: string; @@ -64,7 +64,11 @@ const integrationWorkflowData: Array<{ }, ]; -export const JiraImporterRoot = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const JiraImporterRoot: React.FC = ({ user }) => { const [currentStep, setCurrentStep] = useState({ state: "import-configure", }); @@ -85,7 +89,7 @@ export const JiraImporterRoot = () => { if (!workspaceSlug) return; await jiraImporterService - .createJiraImporter(workspaceSlug.toString(), data) + .createJiraImporter(workspaceSlug.toString(), data, user) .then(() => { mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString())); router.push(`/${workspaceSlug}/settings/import-export`); diff --git a/apps/app/components/issues/activity.tsx b/apps/app/components/issues/activity.tsx index 7a3aed4a6..3b9ecc853 100644 --- a/apps/app/components/issues/activity.tsx +++ b/apps/app/components/issues/activity.tsx @@ -1,6 +1,7 @@ import React from "react"; + import { useRouter } from "next/router"; -import Image from "next/image"; + import useSWR from "swr"; // icons @@ -27,7 +28,7 @@ import { Loader } from "components/ui"; import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssueComment, IIssueLabels } from "types"; +import { ICurrentUserResponse, IIssueComment, IIssueLabels } from "types"; import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import useEstimateOption from "hooks/use-estimate-option"; @@ -110,7 +111,11 @@ const activityDetails: { }, }; -export const IssueActivitySection: React.FC = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const IssueActivitySection: React.FC = ({ user }) => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -143,7 +148,8 @@ export const IssueActivitySection: React.FC = () => { projectId as string, issueId as string, comment.id, - comment + comment, + user ) .then((res) => { mutateIssueActivities(); @@ -160,7 +166,8 @@ export const IssueActivitySection: React.FC = () => { workspaceSlug as string, projectId as string, issueId as string, - commentId + commentId, + user ) .then(() => mutateIssueActivities()); }; @@ -340,7 +347,7 @@ export const IssueActivitySection: React.FC = () => { ?.icon ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.first_name} = { comment_html: "", }; -export const AddComment: React.FC = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const AddComment: React.FC = ({ user }) => { const { handleSubmit, control, @@ -67,7 +71,13 @@ export const AddComment: React.FC = () => { ) return; await issuesServices - .createIssueComment(workspaceSlug as string, projectId as string, issueId as string, formData) + .createIssueComment( + workspaceSlug as string, + projectId as string, + issueId as string, + formData, + user + ) .then(() => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); reset(defaultValues); diff --git a/apps/app/components/issues/comment/comment-card.tsx b/apps/app/components/issues/comment/comment-card.tsx index e632e9ad3..72f6fcae6 100644 --- a/apps/app/components/issues/comment/comment-card.tsx +++ b/apps/app/components/issues/comment/comment-card.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; -import Image from "next/image"; import dynamic from "next/dynamic"; // react-hook-form @@ -68,12 +67,12 @@ export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentD
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? ( - {comment.actor_detail.first_name} ) : (
void; data: IIssue | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) => { +export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -57,7 +58,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) if (!workspaceSlug || !projectId || !data) return; await issueServices - .deleteIssue(workspaceSlug as string, projectId as string, data.id) + .deleteIssue(workspaceSlug as string, projectId as string, data.id, user) .then(() => { if (issueView === "calendar") { const calendarFetchKey = cycleId diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index f09e5aedd..894830bd7 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -39,7 +39,7 @@ import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline"; // helpers import { cosineSimilarity } from "helpers/string.helper"; // types -import type { IIssue } from "types"; +import type { ICurrentUserResponse, IIssue } from "types"; // rich-text-editor const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { ssr: false, @@ -91,6 +91,7 @@ export interface IssueFormProps { setCreateMore: React.Dispatch>; handleClose: () => void; status: boolean; + user: ICurrentUserResponse | undefined; } export const IssueForm: FC = ({ @@ -103,6 +104,7 @@ export const IssueForm: FC = ({ setCreateMore, handleClose, status, + user, }) => { // states const [mostSimilarIssue, setMostSimilarIssue] = useState(); @@ -177,10 +179,15 @@ export const IssueForm: FC = ({ setIAmFeelingLucky(true); aiService - .createGptTask(workspaceSlug as string, projectId as string, { - prompt: issueName, - task: "Generate a proper description for this issue in context of a project management software.", - }) + .createGptTask( + workspaceSlug as string, + projectId as string, + { + prompt: issueName, + task: "Generate a proper description for this issue in context of a project management software.", + }, + user + ) .then((res) => { if (res.response === "") setToastAlert({ @@ -227,12 +234,18 @@ export const IssueForm: FC = ({ isOpen={stateModal} handleClose={() => setStateModal(false)} projectId={projectId} + user={user} + /> + setCycleModal(false)} + user={user} /> - setCycleModal(false)} /> setLabelModal(false)} projectId={projectId} + user={user} /> )} diff --git a/apps/app/components/issues/gantt-chart.tsx b/apps/app/components/issues/gantt-chart.tsx index 1330c2438..571583707 100644 --- a/apps/app/components/issues/gantt-chart.tsx +++ b/apps/app/components/issues/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartIssues from "hooks/gantt-chart/issue-view"; @@ -37,9 +39,23 @@ export const IssueGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )}
); @@ -51,17 +67,25 @@ export const IssueGanttChartView: FC = ({}) => { start_date: data?.start_date, target_date: data?.target_date, }; - - console.log("payload", payload); }; const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index de54e65b3..3210e350a 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -102,9 +102,15 @@ export const CreateUpdateIssueModal: React.FC = ({ if (!workspaceSlug || !projectId) return; await issuesService - .addIssueToCycle(workspaceSlug as string, activeProject ?? "", cycleId, { - issues: [issueId], - }) + .addIssueToCycle( + workspaceSlug as string, + activeProject ?? "", + cycleId, + { + issues: [issueId], + }, + user + ) .then(() => { if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params)); @@ -117,9 +123,15 @@ export const CreateUpdateIssueModal: React.FC = ({ if (!workspaceSlug || !projectId) return; await modulesService - .addIssuesToModule(workspaceSlug as string, activeProject ?? "", moduleId as string, { - issues: [issueId], - }) + .addIssuesToModule( + workspaceSlug as string, + activeProject ?? "", + moduleId as string, + { + issues: [issueId], + }, + user + ) .then(() => { if (moduleId) { mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); @@ -148,7 +160,7 @@ export const CreateUpdateIssueModal: React.FC = ({ if (!workspaceSlug) return; await issuesService - .createIssues(workspaceSlug as string, activeProject ?? "", payload) + .createIssues(workspaceSlug as string, activeProject ?? "", payload, user) .then(async (res) => { mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); @@ -180,13 +192,13 @@ export const CreateUpdateIssueModal: React.FC = ({ const updateIssue = async (payload: Partial) => { await issuesService - .updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload) + .updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user) .then((res) => { if (isUpdatingSingleIssue) { mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); } else { if (issueView === "calendar") mutate(calendarFetchKey); - else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); + mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); } if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); @@ -261,6 +273,7 @@ export const CreateUpdateIssueModal: React.FC = ({ projectId={activeProject ?? ""} setActiveProject={setActiveProject} status={data ? true : false} + user={user} /> diff --git a/apps/app/components/issues/my-issues-list-item.tsx b/apps/app/components/issues/my-issues-list-item.tsx index 06cd6f87f..e026b2f89 100644 --- a/apps/app/components/issues/my-issues-list-item.tsx +++ b/apps/app/components/issues/my-issues-list-item.tsx @@ -7,6 +7,7 @@ import { mutate } from "swr"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // services import issuesService from "services/issues.service"; // components @@ -37,6 +38,9 @@ type Props = { export const MyIssuesListItem: React.FC = ({ issue, properties, projectId }) => { const router = useRouter(); const { workspaceSlug } = router.query; + + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const partialUpdateIssue = useCallback( @@ -55,7 +59,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData) + .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) .then((res) => { mutate(USER_ISSUE(workspaceSlug as string)); }) @@ -110,6 +114,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId )} @@ -117,6 +122,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId )} @@ -124,6 +130,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId )} diff --git a/apps/app/components/issues/sidebar-select/cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx index 3396dde27..0d8beaab8 100644 --- a/apps/app/components/issues/sidebar-select/cycle.tsx +++ b/apps/app/components/issues/sidebar-select/cycle.tsx @@ -16,7 +16,7 @@ import { ContrastIcon } from "components/icons"; // types import { ICycle, IIssue, UserAuth } from "types"; // fetch-keys -import { CYCLE_ISSUES, CYCLE_INCOMPLETE_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; +import { CYCLE_ISSUES, INCOMPLETE_CYCLES_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; type Props = { issueDetail: IIssue | undefined; @@ -33,9 +33,14 @@ export const SidebarCycleSelect: React.FC = ({ const { workspaceSlug, projectId, issueId } = router.query; const { data: incompleteCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_INCOMPLETE_LIST(projectId as string) : null, + workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cyclesService.getIncompleteCycles(workspaceSlug as string, projectId as string) + ? () => + cyclesService.getCyclesWithParams( + workspaceSlug as string, + projectId as string, + "incomplete" + ) : null ); diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index 869dda5c1..5b15fd46f 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -12,6 +12,7 @@ import { TwitterPicker } from "react-color"; import { Popover, Listbox, Transition } from "@headlessui/react"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // services import issuesService from "services/issues.service"; import modulesService from "services/modules.service"; @@ -76,6 +77,8 @@ export const IssueDetailsSidebar: React.FC = ({ const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; + const { user } = useUserAuth(); + const { memberRole } = useProjectMyMembership(); const { setToastAlert } = useToast(); @@ -110,7 +113,7 @@ export const IssueDetailsSidebar: React.FC = ({ const handleNewLabel = (formData: any) => { if (!workspaceSlug || !projectId || isSubmitting) return; issuesService - .createIssueLabel(workspaceSlug as string, projectId as string, formData) + .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) .then((res) => { reset(defaultValues); issueLabelMutate((prevData) => [...(prevData ?? []), res], false); @@ -124,9 +127,15 @@ export const IssueDetailsSidebar: React.FC = ({ if (!workspaceSlug || !projectId || !issueDetail) return; issuesService - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleDetails.id, { - issues: [issueDetail.id], - }) + .addIssueToCycle( + workspaceSlug as string, + projectId as string, + cycleDetails.id, + { + issues: [issueDetail.id], + }, + user + ) .then((res) => { mutate(ISSUE_DETAILS(issueId as string)); }); @@ -139,9 +148,15 @@ export const IssueDetailsSidebar: React.FC = ({ if (!workspaceSlug || !projectId || !issueDetail) return; modulesService - .addIssuesToModule(workspaceSlug as string, projectId as string, moduleDetail.id, { - issues: [issueDetail.id], - }) + .addIssuesToModule( + workspaceSlug as string, + projectId as string, + moduleDetail.id, + { + issues: [issueDetail.id], + }, + user + ) .then((res) => { mutate(ISSUE_DETAILS(issueId as string)); }); @@ -228,6 +243,7 @@ export const IssueDetailsSidebar: React.FC = ({ handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueDetail ?? null} + user={user} />
diff --git a/apps/app/components/issues/sub-issues-list.tsx b/apps/app/components/issues/sub-issues-list.tsx index 85f05a3c9..76424767e 100644 --- a/apps/app/components/issues/sub-issues-list.tsx +++ b/apps/app/components/issues/sub-issues-list.tsx @@ -21,15 +21,16 @@ import { ChevronRightIcon, PlusIcon, XMarkIcon } from "@heroicons/react/24/outli // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { IIssue, ISubIssueResponse } from "types"; +import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types"; // fetch-keys import { PROJECT_ISSUES_LIST, SUB_ISSUES } from "constants/fetch-keys"; type Props = { parentIssue: IIssue; + user: ICurrentUserResponse | undefined; }; -export const SubIssuesList: FC = ({ parentIssue }) => { +export const SubIssuesList: FC = ({ parentIssue, user }) => { // states const [createIssueModal, setCreateIssueModal] = useState(false); const [subIssuesListModal, setSubIssuesListModal] = useState(false); @@ -134,7 +135,7 @@ export const SubIssuesList: FC = ({ parentIssue }) => { ); issuesService - .patchIssue(workspaceSlug.toString(), projectId.toString(), issueId, { parent: null }) + .patchIssue(workspaceSlug.toString(), projectId.toString(), issueId, { parent: null }, user) .then((res) => { mutate(SUB_ISSUES(parentIssue.id ?? "")); @@ -165,15 +166,10 @@ export const SubIssuesList: FC = ({ parentIssue }) => { }); }; - const completedSubIssues = - subIssuesResponse && subIssuesResponse.state_distribution - ? (subIssuesResponse?.state_distribution.completed - ? subIssuesResponse?.state_distribution.completed - : 0) + - (subIssuesResponse?.state_distribution.cancelled - ? subIssuesResponse?.state_distribution.cancelled - : 0) - : 0; + const completedSubIssues = subIssuesResponse + ? subIssuesResponse?.state_distribution.completed + + subIssuesResponse?.state_distribution.cancelled + : 0; const totalSubIssues = subIssuesResponse && subIssuesResponse.sub_issues ? subIssuesResponse?.sub_issues.length : 0; @@ -278,7 +274,7 @@ export const SubIssuesList: FC = ({ parentIssue }) => { key={issue.id} href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`} > - +
= ({ position = "left", selfPositioned = false, tooltipPosition = "right", + user, isNotAllowed, }) => { const router = useRouter(); @@ -83,7 +85,8 @@ export const ViewAssigneeSelect: React.FC = ({ projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_ASSIGNEE" + "ISSUE_PROPERTY_UPDATE_ASSIGNEE", + user ); }} options={options} @@ -123,7 +126,7 @@ export const ViewAssigneeSelect: React.FC = ({ position={position} disabled={isNotAllowed} selfPositioned={selfPositioned} - dropdownWidth="w-full min-w-[8rem]" + dropdownWidth="w-full min-w-[12rem]" /> ); }; diff --git a/apps/app/components/issues/view-select/due-date.tsx b/apps/app/components/issues/view-select/due-date.tsx index 60fa8c718..bea5ff045 100644 --- a/apps/app/components/issues/view-select/due-date.tsx +++ b/apps/app/components/issues/view-select/due-date.tsx @@ -7,15 +7,21 @@ import { findHowManyDaysLeft } from "helpers/date-time.helper"; // services import trackEventServices from "services/track-event.service"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issueId: string) => void; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; -export const ViewDueDateSelect: React.FC = ({ issue, partialUpdateIssue, isNotAllowed }) => { +export const ViewDueDateSelect: React.FC = ({ + issue, + partialUpdateIssue, + user, + isNotAllowed, +}) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -51,7 +57,8 @@ export const ViewDueDateSelect: React.FC = ({ issue, partialUpdateIssue, projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_DUE_DATE" + "ISSUE_PROPERTY_UPDATE_DUE_DATE", + user ); }} className={issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"} diff --git a/apps/app/components/issues/view-select/estimate.tsx b/apps/app/components/issues/view-select/estimate.tsx index 586f930bc..914a5286e 100644 --- a/apps/app/components/issues/view-select/estimate.tsx +++ b/apps/app/components/issues/view-select/estimate.tsx @@ -11,13 +11,14 @@ import { CustomSelect, Tooltip } from "components/ui"; // icons import { PlayIcon } from "@heroicons/react/24/outline"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issueId: string) => void; position?: "left" | "right"; selfPositioned?: boolean; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -26,6 +27,7 @@ export const ViewEstimateSelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, + user, isNotAllowed, }) => { const router = useRouter(); @@ -51,7 +53,8 @@ export const ViewEstimateSelect: React.FC = ({ projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_ESTIMATE" + "ISSUE_PROPERTY_UPDATE_ESTIMATE", + user ); }} label={ diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index aa9a5306e..a0c5cd47c 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -7,7 +7,7 @@ import { CustomSelect, Tooltip } from "components/ui"; // icons import { getPriorityIcon } from "components/icons/priority-icon"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // constants import { PRIORITIES } from "constants/project"; // services @@ -18,6 +18,7 @@ type Props = { partialUpdateIssue: (formData: Partial, issueId: string) => void; position?: "left" | "right"; selfPositioned?: boolean; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -26,6 +27,7 @@ export const ViewPrioritySelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, + user, isNotAllowed, }) => { const router = useRouter(); @@ -45,7 +47,8 @@ export const ViewPrioritySelect: React.FC = ({ projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_PRIORITY" + "ISSUE_PROPERTY_UPDATE_PRIORITY", + user ); }} maxHeight="md" diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index 68a99ac72..2b904eb1e 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -13,7 +13,7 @@ import { getStateGroupIcon } from "components/icons"; import { addSpaceIfCamelCase } from "helpers/string.helper"; import { getStatesList } from "helpers/state.helper"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; @@ -22,6 +22,7 @@ type Props = { partialUpdateIssue: (formData: Partial, issueId: string) => void; position?: "left" | "right"; selfPositioned?: boolean; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -30,6 +31,7 @@ export const ViewStateSelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, + user, isNotAllowed, }) => { const router = useRouter(); @@ -77,21 +79,25 @@ export const ViewStateSelect: React.FC = ({ projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_STATE" + "ISSUE_PROPERTY_UPDATE_STATE", + user ); const oldState = states.find((s) => s.id === issue.state); const newState = states.find((s) => s.id === data); if (oldState?.group !== "completed" && newState?.group !== "completed") { - trackEventServices.trackIssueMarkedAsDoneEvent({ - workspaceSlug: issue.workspace_detail.slug, - workspaceId: issue.workspace_detail.id, - projectId: issue.project_detail.id, - projectIdentifier: issue.project_detail.identifier, - projectName: issue.project_detail.name, - issueId: issue.id, - }); + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug: issue.workspace_detail.slug, + workspaceId: issue.workspace_detail.id, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + user + ); } }} options={options} diff --git a/apps/app/components/labels/create-label-modal.tsx b/apps/app/components/labels/create-label-modal.tsx index 858f7f7b5..c7566dc77 100644 --- a/apps/app/components/labels/create-label-modal.tsx +++ b/apps/app/components/labels/create-label-modal.tsx @@ -17,7 +17,7 @@ import { Input, PrimaryButton, SecondaryButton } from "components/ui"; // icons import { ChevronDownIcon } from "@heroicons/react/24/outline"; // types -import type { IIssueLabels, IState } from "types"; +import type { ICurrentUserResponse, IIssueLabels, IState } from "types"; // constants import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; @@ -26,6 +26,7 @@ type Props = { isOpen: boolean; projectId: string; handleClose: () => void; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { @@ -33,7 +34,7 @@ const defaultValues: Partial = { color: "#858E96", }; -export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClose }) => { +export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClose, user }) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -57,7 +58,7 @@ export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClo if (!workspaceSlug) return; await issuesService - .createIssueLabel(workspaceSlug as string, projectId as string, formData) + .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) .then((res) => { mutate( PROJECT_ISSUE_LABELS(projectId), diff --git a/apps/app/components/labels/create-update-label-inline.tsx b/apps/app/components/labels/create-update-label-inline.tsx index 7010d564e..7b1a04b91 100644 --- a/apps/app/components/labels/create-update-label-inline.tsx +++ b/apps/app/components/labels/create-update-label-inline.tsx @@ -6,6 +6,8 @@ import { mutate } from "swr"; // react-hook-form import { Controller, SubmitHandler, useForm } from "react-hook-form"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // react-color import { TwitterPicker } from "react-color"; // headless ui @@ -42,6 +44,8 @@ export const CreateUpdateLabelInline = forwardRef(function CreateUpd const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { handleSubmit, control, @@ -58,7 +62,7 @@ export const CreateUpdateLabelInline = forwardRef(function CreateUpd if (!workspaceSlug || !projectId || isSubmitting) return; await issuesService - .createIssueLabel(workspaceSlug as string, projectId as string, formData) + .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) .then((res) => { mutate( PROJECT_ISSUE_LABELS(projectId as string), @@ -78,7 +82,8 @@ export const CreateUpdateLabelInline = forwardRef(function CreateUpd workspaceSlug as string, projectId as string, labelToUpdate?.id ?? "", - formData + formData, + user ) .then(() => { reset(defaultValues); diff --git a/apps/app/components/labels/delete-label-modal.tsx b/apps/app/components/labels/delete-label-modal.tsx index 772c9d064..dd3df0fc4 100644 --- a/apps/app/components/labels/delete-label-modal.tsx +++ b/apps/app/components/labels/delete-label-modal.tsx @@ -15,7 +15,7 @@ import useToast from "hooks/use-toast"; // ui import { DangerButton, SecondaryButton } from "components/ui"; // types -import type { IIssueLabels } from "types"; +import type { ICurrentUserResponse, IIssueLabels } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; onClose: () => void; data: IIssueLabels | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data }) => { +export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -50,7 +51,7 @@ export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data }) => ); await issuesService - .deleteIssueLabel(workspaceSlug.toString(), projectId.toString(), data.id) + .deleteIssueLabel(workspaceSlug.toString(), projectId.toString(), data.id, user) .then(() => handleClose()) .catch(() => { setIsDeleteLoading(false); diff --git a/apps/app/components/labels/labels-list-modal.tsx b/apps/app/components/labels/labels-list-modal.tsx index 8d3c04672..23f60ab7f 100644 --- a/apps/app/components/labels/labels-list-modal.tsx +++ b/apps/app/components/labels/labels-list-modal.tsx @@ -11,7 +11,7 @@ import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/out // services import issuesService from "services/issues.service"; // types -import { IIssueLabels } from "types"; +import { ICurrentUserResponse, IIssueLabels } from "types"; // constants import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; @@ -19,9 +19,10 @@ type Props = { isOpen: boolean; handleClose: () => void; parent: IIssueLabels | undefined; + user: ICurrentUserResponse | undefined; }; -export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent }) => { +export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent, user }) => { const [query, setQuery] = useState(""); const router = useRouter(); @@ -58,9 +59,15 @@ export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent } ); await issuesService - .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { - parent: parent?.id ?? "", - }) + .patchIssueLabel( + workspaceSlug as string, + projectId as string, + label.id, + { + parent: parent?.id ?? "", + }, + user + ) .then(() => mutate()); }; diff --git a/apps/app/components/labels/single-label-group.tsx b/apps/app/components/labels/single-label-group.tsx index 00837a05c..f1a74f296 100644 --- a/apps/app/components/labels/single-label-group.tsx +++ b/apps/app/components/labels/single-label-group.tsx @@ -20,7 +20,7 @@ import { TrashIcon, } from "@heroicons/react/24/outline"; // types -import { IIssueLabels } from "types"; +import { ICurrentUserResponse, IIssueLabels } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; @@ -30,6 +30,7 @@ type Props = { addLabelToGroup: (parentLabel: IIssueLabels) => void; editLabel: (label: IIssueLabels) => void; handleLabelDelete: () => void; + user: ICurrentUserResponse | undefined; }; export const SingleLabelGroup: React.FC = ({ @@ -38,6 +39,7 @@ export const SingleLabelGroup: React.FC = ({ addLabelToGroup, editLabel, handleLabelDelete, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -57,9 +59,15 @@ export const SingleLabelGroup: React.FC = ({ ); issuesService - .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { - parent: null, - }) + .patchIssueLabel( + workspaceSlug as string, + projectId as string, + label.id, + { + parent: null, + }, + user + ) .then(() => { mutate(PROJECT_ISSUE_LABELS(projectId as string)); }); diff --git a/apps/app/components/modules/delete-module-modal.tsx b/apps/app/components/modules/delete-module-modal.tsx index f8d803732..f2a9ec7ee 100644 --- a/apps/app/components/modules/delete-module-modal.tsx +++ b/apps/app/components/modules/delete-module-modal.tsx @@ -15,7 +15,7 @@ import { SecondaryButton, DangerButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import type { IModule } from "types"; +import type { ICurrentUserResponse, IModule } from "types"; // fetch-keys import { MODULE_LIST } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; data?: IModule; + user: ICurrentUserResponse | undefined; }; -export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data }) => { +export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -50,7 +51,7 @@ export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data }) ); await modulesService - .deleteModule(workspaceSlug as string, projectId as string, data.id) + .deleteModule(workspaceSlug as string, projectId as string, data.id, user) .then(() => { if (moduleId) router.push(`/${workspaceSlug}/projects/${data.project}/modules`); handleClose(); diff --git a/apps/app/components/modules/form.tsx b/apps/app/components/modules/form.tsx index d79748478..e94bc6d72 100644 --- a/apps/app/components/modules/form.tsx +++ b/apps/app/components/modules/form.tsx @@ -23,7 +23,7 @@ type Props = { const defaultValues: Partial = { name: "", description: "", - status: null, + status: "backlog", lead: null, members_list: [], }; diff --git a/apps/app/components/modules/gantt-chart.tsx b/apps/app/components/modules/gantt-chart.tsx index fa92964c1..e24e1dd9a 100644 --- a/apps/app/components/modules/gantt-chart.tsx +++ b/apps/app/components/modules/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view"; @@ -38,9 +40,23 @@ export const ModuleIssuesGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )}
); @@ -59,10 +75,20 @@ export const ModuleIssuesGanttChartView: FC = ({}) => { const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/modules/modal.tsx b/apps/app/components/modules/modal.tsx index 02e23e138..c06be46d5 100644 --- a/apps/app/components/modules/modal.tsx +++ b/apps/app/components/modules/modal.tsx @@ -15,7 +15,7 @@ import modulesService from "services/modules.service"; // hooks import useToast from "hooks/use-toast"; // types -import type { IModule } from "types"; +import type { ICurrentUserResponse, IModule } from "types"; // fetch-keys import { MODULE_LIST } from "constants/fetch-keys"; @@ -23,17 +23,18 @@ type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; data?: IModule; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { name: "", description: "", - status: null, + status: "backlog", lead: null, members_list: [], }; -export const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data }) => { +export const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, user }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -50,7 +51,7 @@ export const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, da const createModule = async (payload: Partial) => { await modulesService - .createModule(workspaceSlug as string, projectId as string, payload) + .createModule(workspaceSlug as string, projectId as string, payload, user) .then(() => { mutate(MODULE_LIST(projectId as string)); handleClose(); @@ -72,7 +73,7 @@ export const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, da const updateModule = async (payload: Partial) => { await modulesService - .updateModule(workspaceSlug as string, projectId as string, data?.id ?? "", payload) + .updateModule(workspaceSlug as string, projectId as string, data?.id ?? "", payload, user) .then((res) => { mutate( MODULE_LIST(projectId as string), diff --git a/apps/app/components/modules/modules-list-gantt-chart.tsx b/apps/app/components/modules/modules-list-gantt-chart.tsx index cb1c7bc07..0f109ba6a 100644 --- a/apps/app/components/modules/modules-list-gantt-chart.tsx +++ b/apps/app/components/modules/modules-list-gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // types import { IModule } from "types"; // constants @@ -38,9 +40,11 @@ export const ModulesListGanttChartView: FC = ({ modules }) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === data.status)?.color }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
); diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index c1b4674b7..f453e4c68 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -37,7 +37,7 @@ import { LinkIcon } from "@heroicons/react/20/solid"; import { renderDateFormat, renderShortDate } from "helpers/date-time.helper"; import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper"; // types -import { IIssue, IModule, ModuleLink } from "types"; +import { ICurrentUserResponse, IIssue, IModule, ModuleLink } from "types"; // fetch-keys import { MODULE_DETAILS } from "constants/fetch-keys"; // constant @@ -56,9 +56,16 @@ type Props = { module?: IModule; isOpen: boolean; moduleIssues?: IIssue[]; + user: ICurrentUserResponse | undefined; }; -export const ModuleDetailsSidebar: React.FC = ({ issues, module, isOpen, moduleIssues }) => { +export const ModuleDetailsSidebar: React.FC = ({ + issues, + module, + isOpen, + moduleIssues, + user, +}) => { const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); @@ -86,7 +93,7 @@ export const ModuleDetailsSidebar: React.FC = ({ issues, module, isOpen, ); modulesService - .patchModule(workspaceSlug as string, projectId as string, moduleId as string, data) + .patchModule(workspaceSlug as string, projectId as string, moduleId as string, data, user) .then(() => mutate(MODULE_DETAILS(moduleId as string))) .catch((e) => console.log(e)); }; @@ -181,6 +188,7 @@ export const ModuleDetailsSidebar: React.FC = ({ issues, module, isOpen, isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={module} + user={user} />
void; + user: ICurrentUserResponse | undefined; }; -export const SingleModuleCard: React.FC = ({ module, handleEditModule }) => { +export const SingleModuleCard: React.FC = ({ module, handleEditModule, user }) => { const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const router = useRouter(); @@ -128,6 +129,7 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule }) isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={module} + user={user} />
diff --git a/apps/app/components/onboarding/invite-members.tsx b/apps/app/components/onboarding/invite-members.tsx index 4fa58db2e..0114b2dcd 100644 --- a/apps/app/components/onboarding/invite-members.tsx +++ b/apps/app/components/onboarding/invite-members.tsx @@ -2,16 +2,17 @@ import { useForm } from "react-hook-form"; import useToast from "hooks/use-toast"; import workspaceService from "services/workspace.service"; -import { IUser } from "types"; +import { ICurrentUserResponse, IUser } from "types"; // ui components import { MultiInput, PrimaryButton, SecondaryButton } from "components/ui"; type Props = { setStep: React.Dispatch>; workspace: any; + user: ICurrentUserResponse | undefined; }; -export const InviteMembers: React.FC = ({ setStep, workspace }) => { +export const InviteMembers: React.FC = ({ setStep, workspace, user }) => { const { setToastAlert } = useToast(); const { @@ -23,7 +24,7 @@ export const InviteMembers: React.FC = ({ setStep, workspace }) => { const onSubmit = async (formData: IUser) => { await workspaceService - .inviteWorkspace(workspace.slug, formData) + .inviteWorkspace(workspace.slug, formData, user) .then(() => { setToastAlert({ type: "success", @@ -45,8 +46,9 @@ export const InviteMembers: React.FC = ({ setStep, workspace }) => { >
-

Invite your team to your workspace.

+

Invite co-workers to your team

+ Email
= ({ data, gradient = false }) => (
diff --git a/apps/app/components/onboarding/user-details.tsx b/apps/app/components/onboarding/user-details.tsx index e8704df59..63df0d968 100644 --- a/apps/app/components/onboarding/user-details.tsx +++ b/apps/app/components/onboarding/user-details.tsx @@ -66,11 +66,18 @@ export const UserDetails: React.FC = ({ user, setStep, setUserRole }) => return (
-
+
-
-
- First name +
+

User Details

+

+ Enter your details as a first step to open your Plane account. +

+
+ +
+
+ First name = ({ user, setStep, setUserRole }) => error={errors.first_name} />
-
- Last name +
+ Last name = ({ user, setStep, setUserRole }) => />
-
+ +
What is your role?
= ({ user, setStep, setUserRole }) => )} /> + {errors.role && {errors.role.message}}
+
>; setWorkspace: React.Dispatch>; + user: ICurrentUserResponse | undefined; }; -export const Workspace: React.FC = ({ setStep, setWorkspace }) => { +export const Workspace: React.FC = ({ setStep, setWorkspace, user }) => { const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); const [invitationsRespond, setInvitationsRespond] = useState([]); const [defaultValues, setDefaultValues] = useState({ @@ -30,6 +30,7 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => { slug: "", company_size: null, }); + const [currentTab, setCurrentTab] = useState("create"); const { data: invitations, mutate } = useSWR(USER_WORKSPACE_INVITATIONS, () => workspaceService.userWorkspaceInvitations() @@ -64,53 +65,72 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => { }); }; + const currentTabValue = (tab: string | null) => { + switch (tab) { + case "join": + return 0; + case "create": + return 1; + default: + return 1; + } + }; + + console.log("invitations:", invitations); + return ( -
+
{ + switch (i) { + case 0: + return setCurrentTab("join"); + case 1: + return setCurrentTab("create"); + default: + return setCurrentTab("create"); + } + }} > - - - `rounded-3xl border px-4 py-2 outline-none ${ - selected - ? "border-brand-accent bg-brand-accent text-white" - : "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1" - }` - } - > - New Workspace - - - `rounded-3xl border px-5 py-2 outline-none ${ - selected - ? "border-brand-accent bg-brand-accent text-white" - : "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1" - }` - } - > - Invited Workspace - + +
+

Workspace

+

+ Create or join the workspace to get started with Plane. +

+
+
+ + `rounded-3xl border px-4 py-2 outline-none ${ + selected + ? "border-brand-accent bg-brand-accent text-white font-medium" + : "border-brand-base bg-brand-base hover:bg-brand-surface-2" + }` + } + > + Invited Workspace + + + `rounded-3xl border px-4 py-2 outline-none ${ + selected + ? "border-brand-accent bg-brand-accent text-white font-medium" + : "border-brand-base bg-brand-base hover:bg-brand-surface-2" + }` + } + > + New Workspace + +
- - { - setWorkspace(res); - setStep(3); - }} - defaultValues={defaultValues} - setDefaultValues={setDefaultValues} - /> - -
-
+
+
{invitations && invitations.length > 0 ? ( invitations.map((invitation) => (
@@ -121,7 +141,7 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => {
{invitation.workspace.logo && invitation.workspace.logo !== "" ? ( - = ({ setStep, setWorkspace }) => { alt={invitation.workspace.name} /> ) : ( - - {invitation.workspace.name.charAt(0)} + + {getFirstCharacters(invitation.workspace.name)} )}
-
{invitation.workspace.name}
+
+ {truncateText(invitation.workspace.name, 30)} +

- Invited by {invitation.workspace.owner.first_name} + Invited by{" "} + {invitation.created_by_detail + ? invitation.created_by_detail.first_name + : invitation.workspace.owner.first_name}

- { + + {/* { + handleInvitation( + invitation, + invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted" + ); + }} + type="button" + className={`${ + invitationsRespond.includes(invitation.id) + ? "bg-brand-surface-2 text-brand-secondary" + : "bg-brand-accent text-white" + } text-sm px-4 py-2 border border-brand-base rounded-3xl`} + + // className="h-4 w-4 rounded border-brand-base text-brand-accent focus:ring-brand-accent" + /> */}
@@ -167,7 +218,7 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => {
)}
-
+
= ({ setStep, setWorkspace }) => {
+ + { + setWorkspace(res); + setStep(3); + }} + defaultValues={defaultValues} + setDefaultValues={setDefaultValues} + user={user} + /> +
diff --git a/apps/app/components/pages/create-block.tsx b/apps/app/components/pages/create-block.tsx index 59bb81898..d4098db93 100644 --- a/apps/app/components/pages/create-block.tsx +++ b/apps/app/components/pages/create-block.tsx @@ -16,7 +16,7 @@ import useToast from "hooks/use-toast"; // ui import { TextArea } from "components/ui"; // types -import { IPageBlock } from "types"; +import { ICurrentUserResponse, IPageBlock } from "types"; // fetch-keys import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; @@ -24,7 +24,11 @@ const defaultValues = { name: "", }; -export const CreateBlock = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const CreateBlock: React.FC = ({ user }) => { const [blockTitle, setBlockTitle] = useState(""); const router = useRouter(); @@ -49,9 +53,15 @@ export const CreateBlock = () => { if (!workspaceSlug || !projectId || !pageId) return; await pagesService - .createPageBlock(workspaceSlug as string, projectId as string, pageId as string, { - name: watch("name"), - }) + .createPageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + { + name: watch("name"), + }, + user + ) .then((res) => { mutate( PAGE_BLOCKS_LIST(pageId as string), diff --git a/apps/app/components/pages/create-update-block-inline.tsx b/apps/app/components/pages/create-update-block-inline.tsx index 528c83f9e..27e6bd419 100644 --- a/apps/app/components/pages/create-update-block-inline.tsx +++ b/apps/app/components/pages/create-update-block-inline.tsx @@ -20,7 +20,7 @@ import { GptAssistantModal } from "components/core"; // ui import { Loader, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; // types -import { IPageBlock } from "types"; +import { ICurrentUserResponse, IPageBlock } from "types"; // fetch-keys import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; @@ -30,6 +30,7 @@ type Props = { handleAiAssistance?: (response: string) => void; setIsSyncing?: React.Dispatch>; focus?: keyof IPageBlock; + user: ICurrentUserResponse | undefined; }; const defaultValues = { @@ -61,6 +62,7 @@ export const CreateUpdateBlockInline: React.FC = ({ handleAiAssistance, setIsSyncing, focus, + user, }) => { const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [gptAssistantModal, setGptAssistantModal] = useState(false); @@ -96,11 +98,17 @@ export const CreateUpdateBlockInline: React.FC = ({ if (!workspaceSlug || !projectId || !pageId) return; await pagesService - .createPageBlock(workspaceSlug as string, projectId as string, pageId as string, { - name: formData.name, - description: formData.description ?? "", - description_html: formData.description_html ?? "

", - }) + .createPageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + { + name: formData.name, + description: formData.description ?? "", + description_html: formData.description_html ?? "

", + }, + user + ) .then((res) => { mutate( PAGE_BLOCKS_LIST(pageId as string), @@ -139,21 +147,34 @@ export const CreateUpdateBlockInline: React.FC = ({ ); await pagesService - .patchPageBlock(workspaceSlug as string, projectId as string, pageId as string, data.id, { - name: formData.name, - description: formData.description, - description_html: formData.description_html, - }) + .patchPageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + data.id, + { + name: formData.name, + description: formData.description, + description_html: formData.description_html, + }, + user + ) .then((res) => { mutate(PAGE_BLOCKS_LIST(pageId as string)); editorRef.current?.setEditorValue(res.description_html); if (data.issue && data.sync) issuesService - .patchIssue(workspaceSlug as string, projectId as string, data.issue, { - name: res.name, - description: res.description, - description_html: res.description_html, - }) + .patchIssue( + workspaceSlug as string, + projectId as string, + data.issue, + { + name: res.name, + description: res.description, + description_html: res.description_html, + }, + user + ) .finally(() => { if (setIsSyncing) setIsSyncing(false); }); @@ -169,10 +190,15 @@ export const CreateUpdateBlockInline: React.FC = ({ setIAmFeelingLucky(true); aiService - .createGptTask(workspaceSlug as string, projectId as string, { - prompt: watch("name"), - task: "Generate a proper description for this issue in context of a project management software.", - }) + .createGptTask( + workspaceSlug as string, + projectId as string, + { + prompt: watch("name"), + task: "Generate a proper description for this issue in context of a project management software.", + }, + user + ) .then((res) => { if (res.response === "") setToastAlert({ diff --git a/apps/app/components/pages/create-update-page-modal.tsx b/apps/app/components/pages/create-update-page-modal.tsx index 8c26e0d00..57e25b5f7 100644 --- a/apps/app/components/pages/create-update-page-modal.tsx +++ b/apps/app/components/pages/create-update-page-modal.tsx @@ -13,7 +13,7 @@ import useToast from "hooks/use-toast"; // components import { PageForm } from "./page-form"; // types -import { IPage } from "types"; +import { ICurrentUserResponse, IPage } from "types"; // fetch-keys import { ALL_PAGES_LIST, @@ -26,9 +26,10 @@ type Props = { isOpen: boolean; handleClose: () => void; data?: IPage | null; + user: ICurrentUserResponse | undefined; }; -export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, data }) => { +export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, data, user }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -40,7 +41,7 @@ export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, da const createPage = async (payload: IPage) => { await pagesService - .createPage(workspaceSlug as string, projectId as string, payload) + .createPage(workspaceSlug as string, projectId as string, payload, user) .then((res) => { mutate(RECENT_PAGES_LIST(projectId as string)); mutate( @@ -82,7 +83,7 @@ export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, da const updatePage = async (payload: IPage) => { await pagesService - .patchPage(workspaceSlug as string, projectId as string, data?.id ?? "", payload) + .patchPage(workspaceSlug as string, projectId as string, data?.id ?? "", payload, user) .then((res) => { mutate(RECENT_PAGES_LIST(projectId as string)); mutate( diff --git a/apps/app/components/pages/delete-page-modal.tsx b/apps/app/components/pages/delete-page-modal.tsx index b2f202284..6277870d1 100644 --- a/apps/app/components/pages/delete-page-modal.tsx +++ b/apps/app/components/pages/delete-page-modal.tsx @@ -15,7 +15,7 @@ import { DangerButton, SecondaryButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import type { IPage } from "types"; +import type { ICurrentUserResponse, IPage } from "types"; // fetch-keys import { ALL_PAGES_LIST, @@ -28,12 +28,14 @@ type TConfirmPageDeletionProps = { isOpen: boolean; setIsOpen: React.Dispatch>; data?: IPage | null; + user: ICurrentUserResponse | undefined; }; export const DeletePageModal: React.FC = ({ isOpen, setIsOpen, data, + user, }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -52,7 +54,7 @@ export const DeletePageModal: React.FC = ({ if (!data || !workspaceSlug || !projectId) return; await pagesService - .deletePage(workspaceSlug as string, data.project, data.id) + .deletePage(workspaceSlug as string, data.project, data.id, user) .then(() => { mutate(RECENT_PAGES_LIST(projectId as string)); mutate( diff --git a/apps/app/components/pages/pages-list/all-pages-list.tsx b/apps/app/components/pages/pages-list/all-pages-list.tsx index ec364eba6..f25d00fd5 100644 --- a/apps/app/components/pages/pages-list/all-pages-list.tsx +++ b/apps/app/components/pages/pages-list/all-pages-list.tsx @@ -18,13 +18,9 @@ export const AllPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? ALL_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getAllPages(workspaceSlug as string, projectId as string) + ? () => pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, "all") : null ); - return ( -
- -
- ); + return ; }; diff --git a/apps/app/components/pages/pages-list/favorite-pages-list.tsx b/apps/app/components/pages/pages-list/favorite-pages-list.tsx index bf0ab8133..2faa4bf72 100644 --- a/apps/app/components/pages/pages-list/favorite-pages-list.tsx +++ b/apps/app/components/pages/pages-list/favorite-pages-list.tsx @@ -18,13 +18,10 @@ export const FavoritePagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? FAVORITE_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getFavoritePages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, "favorite") : null ); - return ( -
- -
- ); + return ; }; diff --git a/apps/app/components/pages/pages-list/my-pages-list.tsx b/apps/app/components/pages/pages-list/my-pages-list.tsx index 41704e52e..c225a0ac5 100644 --- a/apps/app/components/pages/pages-list/my-pages-list.tsx +++ b/apps/app/components/pages/pages-list/my-pages-list.tsx @@ -18,13 +18,14 @@ export const MyPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? MY_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getMyPages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams( + workspaceSlug as string, + projectId as string, + "created_by_me" + ) : null ); - return ( -
- -
- ); + return ; }; diff --git a/apps/app/components/pages/pages-list/other-pages-list.tsx b/apps/app/components/pages/pages-list/other-pages-list.tsx index 7cf21a3e2..64764533f 100644 --- a/apps/app/components/pages/pages-list/other-pages-list.tsx +++ b/apps/app/components/pages/pages-list/other-pages-list.tsx @@ -18,13 +18,14 @@ export const OtherPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? OTHER_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getOtherPages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams( + workspaceSlug as string, + projectId as string, + "created_by_other" + ) : null ); - return ( -
- -
- ); + return ; }; diff --git a/apps/app/components/pages/pages-list/recent-pages-list.tsx b/apps/app/components/pages/pages-list/recent-pages-list.tsx index 7b162f796..44225aee5 100644 --- a/apps/app/components/pages/pages-list/recent-pages-list.tsx +++ b/apps/app/components/pages/pages-list/recent-pages-list.tsx @@ -37,37 +37,28 @@ export const RecentPagesList: React.FC = ({ viewType }) => { <> {pages ? ( Object.keys(pages).length > 0 && !isEmpty ? ( -
- {Object.keys(pages).map((key) => { - if (pages[key].length === 0) return null; + Object.keys(pages).map((key) => { + if (pages[key].length === 0) return null; - return ( - -
-

- {replaceUnderscoreIfSnakeCase(key)} -

- -
-
- ); - })} -
+ return ( +
+

+ {replaceUnderscoreIfSnakeCase(key)} +

+ +
+ ); + }) ) : ( -
- -
+ ) ) : ( - + diff --git a/apps/app/components/pages/pages-view.tsx b/apps/app/components/pages/pages-view.tsx index 58aa21c07..7d1eac724 100644 --- a/apps/app/components/pages/pages-view.tsx +++ b/apps/app/components/pages/pages-view.tsx @@ -8,6 +8,7 @@ import pagesService from "services/pages.service"; import projectService from "services/project.service"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // components import { CreateUpdatePageModal, @@ -44,6 +45,8 @@ export const PagesView: React.FC = ({ pages, viewType }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: people } = useSWR( @@ -181,7 +184,7 @@ export const PagesView: React.FC = ({ pages, viewType }) => { ); pagesService - .patchPage(workspaceSlug.toString(), projectId.toString(), page.id, formData) + .patchPage(workspaceSlug.toString(), projectId.toString(), page.id, formData, user) .then(() => { mutate(RECENT_PAGES_LIST(projectId.toString())); }); @@ -193,68 +196,72 @@ export const PagesView: React.FC = ({ pages, viewType }) => { isOpen={createUpdatePageModal} handleClose={() => setCreateUpdatePageModal(false)} data={selectedPageToUpdate} + user={user} /> {pages ? ( - pages.length > 0 ? ( - viewType === "list" ? ( -
    - {pages.map((page) => ( - handleEditPage(page)} - handleDeletePage={() => handleDeletePage(page)} - handleAddToFavorites={() => handleAddToFavorites(page)} - handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} - partialUpdatePage={partialUpdatePage} - /> - ))} -
- ) : viewType === "detailed" ? ( -
- {pages.map((page) => ( - handleEditPage(page)} - handleDeletePage={() => handleDeletePage(page)} - handleAddToFavorites={() => handleAddToFavorites(page)} - handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} - partialUpdatePage={partialUpdatePage} - /> - ))} -
+
+ {pages.length > 0 ? ( + viewType === "list" ? ( +
    + {pages.map((page) => ( + handleEditPage(page)} + handleDeletePage={() => handleDeletePage(page)} + handleAddToFavorites={() => handleAddToFavorites(page)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} + partialUpdatePage={partialUpdatePage} + /> + ))} +
+ ) : viewType === "detailed" ? ( +
+ {pages.map((page) => ( + handleEditPage(page)} + handleDeletePage={() => handleDeletePage(page)} + handleAddToFavorites={() => handleAddToFavorites(page)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} + partialUpdatePage={partialUpdatePage} + /> + ))} +
+ ) : ( +
+ {pages.map((page) => ( + handleEditPage(page)} + handleDeletePage={() => handleDeletePage(page)} + handleAddToFavorites={() => handleAddToFavorites(page)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} + partialUpdatePage={partialUpdatePage} + /> + ))} +
+ ) ) : ( -
- {pages.map((page) => ( - handleEditPage(page)} - handleDeletePage={() => handleDeletePage(page)} - handleAddToFavorites={() => handleAddToFavorites(page)} - handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} - partialUpdatePage={partialUpdatePage} - /> - ))} -
- ) - ) : ( - - ) + + )} +
) : viewType === "list" ? ( diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx index 42f0e5523..3efbd33eb 100644 --- a/apps/app/components/pages/single-page-block.tsx +++ b/apps/app/components/pages/single-page-block.tsx @@ -35,7 +35,7 @@ import { // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IIssue, IPageBlock, IProject } from "types"; +import { ICurrentUserResponse, IIssue, IPageBlock, IProject } from "types"; // fetch-keys import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; @@ -43,9 +43,10 @@ type Props = { block: IPageBlock; projectDetails: IProject | undefined; index: number; + user: ICurrentUserResponse | undefined; }; -export const SinglePageBlock: React.FC = ({ block, projectDetails, index }) => { +export const SinglePageBlock: React.FC = ({ block, projectDetails, index, user }) => { const [isSyncing, setIsSyncing] = useState(false); const [createBlockForm, setCreateBlockForm] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); @@ -87,20 +88,33 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index ); await pagesService - .patchPageBlock(workspaceSlug as string, projectId as string, pageId as string, block.id, { - name: formData.name, - description: formData.description, - description_html: formData.description_html, - }) + .patchPageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + block.id, + { + name: formData.name, + description: formData.description, + description_html: formData.description_html, + }, + user + ) .then((res) => { mutate(PAGE_BLOCKS_LIST(pageId as string)); if (block.issue && block.sync) issuesService - .patchIssue(workspaceSlug as string, projectId as string, block.issue, { - name: res.name, - description: res.description, - description_html: res.description_html, - }) + .patchIssue( + workspaceSlug as string, + projectId as string, + block.issue, + { + name: res.name, + description: res.description, + description_html: res.description_html, + }, + user + ) .finally(() => setIsSyncing(false)); }); }; @@ -113,7 +127,8 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index workspaceSlug as string, projectId as string, pageId as string, - block.id + block.id, + user ) .then((res: IIssue) => { mutate( @@ -152,7 +167,13 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index ); await pagesService - .deletePageBlock(workspaceSlug as string, projectId as string, pageId as string, block.id) + .deletePageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + block.id, + user + ) .catch(() => { setToastAlert({ type: "error", @@ -168,10 +189,15 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index setIAmFeelingLucky(true); aiService - .createGptTask(workspaceSlug as string, projectId as string, { - prompt: block.name, - task: "Generate a proper description for this issue in context of a project management software.", - }) + .createGptTask( + workspaceSlug as string, + projectId as string, + { + prompt: block.name, + task: "Generate a proper description for this issue in context of a project management software.", + }, + user + ) .then((res) => { if (res.response === "") setToastAlert({ @@ -243,7 +269,8 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index block.id, { sync: !block.sync, - } + }, + user ); }; @@ -281,6 +308,7 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index data={block} setIsSyncing={setIsSyncing} focus="name" + user={user} />
) : ( diff --git a/apps/app/components/project/create-project-modal.tsx b/apps/app/components/project/create-project-modal.tsx index c731c12f0..06fc73348 100644 --- a/apps/app/components/project/create-project-modal.tsx +++ b/apps/app/components/project/create-project-modal.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from "react"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; @@ -24,7 +23,7 @@ import EmojiIconPicker from "components/emoji-icon-picker"; // helpers import { getRandomEmoji } from "helpers/common.helper"; // types -import { IProject } from "types"; +import { ICurrentUserResponse, IProject } from "types"; // fetch-keys import { PROJECTS_LIST, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; // constants @@ -33,6 +32,7 @@ import { NETWORK_CHOICES } from "constants/project"; type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { @@ -63,7 +63,7 @@ const IsGuestCondition: React.FC<{ }; export const CreateProjectModal: React.FC = (props) => { - const { isOpen, setIsOpen } = props; + const { isOpen, setIsOpen, user } = props; const [isChangeIdentifierRequired, setIsChangeIdentifierRequired] = useState(true); @@ -120,7 +120,7 @@ export const CreateProjectModal: React.FC = (props) => { else payload.emoji = formData.emoji_and_icon; await projectServices - .createProject(workspaceSlug as string, payload) + .createProject(workspaceSlug as string, payload, user) .then((res) => { mutate( PROJECTS_LIST(workspaceSlug as string), @@ -185,14 +185,12 @@ export const CreateProjectModal: React.FC = (props) => { leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
{watch("cover_image") !== null && ( - cover image )} diff --git a/apps/app/components/project/delete-project-modal.tsx b/apps/app/components/project/delete-project-modal.tsx index 49d3f745b..5a4be1706 100644 --- a/apps/app/components/project/delete-project-modal.tsx +++ b/apps/app/components/project/delete-project-modal.tsx @@ -13,7 +13,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui import { DangerButton, Input, SecondaryButton } from "components/ui"; // types -import type { IProject, IWorkspace } from "types"; +import type { ICurrentUserResponse, IProject, IWorkspace } from "types"; // fetch-keys import { PROJECTS_LIST } from "constants/fetch-keys"; @@ -22,6 +22,7 @@ type TConfirmProjectDeletionProps = { onClose: () => void; onSuccess?: () => void; data: IProject | null; + user: ICurrentUserResponse | undefined; }; export const DeleteProjectModal: React.FC = ({ @@ -29,6 +30,7 @@ export const DeleteProjectModal: React.FC = ({ data, onClose, onSuccess, + user, }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [confirmProjectName, setConfirmProjectName] = useState(""); @@ -65,7 +67,7 @@ export const DeleteProjectModal: React.FC = ({ setIsDeleteLoading(true); if (!data || !workspaceSlug || !canDelete) return; await projectService - .deleteProject(workspaceSlug, data.id) + .deleteProject(workspaceSlug, data.id, user) .then(() => { handleClose(); mutate(PROJECTS_LIST(workspaceSlug), (prevData) => diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx index e18e7480e..e08b92e8c 100644 --- a/apps/app/components/project/send-project-invitation-modal.tsx +++ b/apps/app/components/project/send-project-invitation-modal.tsx @@ -15,7 +15,7 @@ import useToast from "hooks/use-toast"; import projectService from "services/project.service"; import workspaceService from "services/workspace.service"; // types -import { IProjectMemberInvitation } from "types"; +import { ICurrentUserResponse, IProjectMemberInvitation } from "types"; // fetch-keys import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // constants @@ -25,6 +25,7 @@ type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; members: any[]; + user: ICurrentUserResponse | undefined; }; type ProjectMember = IProjectMemberInvitation & { @@ -40,7 +41,7 @@ const defaultValues: Partial = { user_id: "", }; -const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, members }) => { +const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, members, user }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -70,12 +71,15 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member const onSubmit = async (formData: ProjectMember) => { if (!workspaceSlug || !projectId || isSubmitting) return; await projectService - .inviteProject(workspaceSlug as string, projectId as string, formData) + .inviteProject(workspaceSlug as string, projectId as string, formData, user) .then((response) => { setIsOpen(false); - mutate( + mutate( PROJECT_INVITATIONS, - (prevData: any[]) => [{ ...formData, ...response }, ...(prevData ?? [])], + (prevData) => { + if (!prevData) return prevData; + return [{ ...formData, ...response }, ...(prevData ?? [])]; + }, false ); setToastAlert({ @@ -162,14 +166,22 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member input width="w-full" > - {uninvitedPeople?.map((person) => ( - - {person.member.email} - - ))} + {uninvitedPeople && uninvitedPeople.length > 0 ? ( + <> + {uninvitedPeople?.map((person) => ( + + {person.member.email} + + ))} + + ) : ( +
+ Invite members to workspace before adding them to a project. +
+ )} )} /> diff --git a/apps/app/components/project/sidebar-list.tsx b/apps/app/components/project/sidebar-list.tsx index 9d4b9f415..f9b6c5760 100644 --- a/apps/app/components/project/sidebar-list.tsx +++ b/apps/app/components/project/sidebar-list.tsx @@ -9,6 +9,7 @@ import { PlusIcon } from "@heroicons/react/24/outline"; // hooks import useToast from "hooks/use-toast"; import useTheme from "hooks/use-theme"; +import useUserAuth from "hooks/use-user-auth"; // services import projectService from "services/project.service"; // components @@ -29,6 +30,9 @@ export const ProjectSidebarList: FC = () => { // router const router = useRouter(); const { workspaceSlug } = router.query; + + const { user } = useUserAuth(); + // states const [isCreateProjectModal, setCreateProjectModal] = useState(false); // theme @@ -136,11 +140,16 @@ export const ProjectSidebarList: FC = () => { return ( <> - + setDeleteProjectModal(false)} data={projectToDelete} + user={user} />
{favoriteProjects && favoriteProjects.length > 0 && ( diff --git a/apps/app/components/project/single-project-card.tsx b/apps/app/components/project/single-project-card.tsx index c2844212d..66ef6aa2a 100644 --- a/apps/app/components/project/single-project-card.tsx +++ b/apps/app/components/project/single-project-card.tsx @@ -2,7 +2,6 @@ import React from "react"; import { useRouter } from "next/router"; import Link from "next/link"; -import Image from "next/image"; import { mutate } from "swr"; @@ -142,15 +141,13 @@ export const SingleProjectCard: React.FC = ({
- {project.name}
{!hasJoined ? ( diff --git a/apps/app/components/search-listbox/index.tsx b/apps/app/components/search-listbox/index.tsx index ef7c33634..cbbc6cf5b 100644 --- a/apps/app/components/search-listbox/index.tsx +++ b/apps/app/components/search-listbox/index.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -62,12 +61,10 @@ const SearchListbox: React.FC = ({ if (user.member.avatar && user.member.avatar !== "") { return (
- avatar
); @@ -151,7 +148,9 @@ const SearchListbox: React.FC = ({ )) ) : ( -

No {title.toLowerCase()} found

+

+ No {title.toLowerCase()} found +

) ) : (

Loading...

diff --git a/apps/app/components/states/create-state-modal.tsx b/apps/app/components/states/create-state-modal.tsx index ed62bb9ac..dca001932 100644 --- a/apps/app/components/states/create-state-modal.tsx +++ b/apps/app/components/states/create-state-modal.tsx @@ -19,7 +19,7 @@ import { CustomSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "c // icons import { ChevronDownIcon } from "@heroicons/react/24/outline"; // types -import type { IState, IStateResponse } from "types"; +import type { ICurrentUserResponse, IState, IStateResponse } from "types"; // fetch keys import { STATES_LIST } from "constants/fetch-keys"; // constants @@ -30,6 +30,7 @@ type Props = { isOpen: boolean; projectId: string; handleClose: () => void; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { @@ -39,7 +40,7 @@ const defaultValues: Partial = { group: "backlog", }; -export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClose }) => { +export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClose, user }) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -69,7 +70,7 @@ export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClo }; await stateService - .createState(workspaceSlug as string, projectId, payload) + .createState(workspaceSlug as string, projectId, payload, user) .then((res) => { mutate( STATES_LIST(projectId.toString()), diff --git a/apps/app/components/states/create-update-state-inline.tsx b/apps/app/components/states/create-update-state-inline.tsx index 42ab52945..8a9d81968 100644 --- a/apps/app/components/states/create-update-state-inline.tsx +++ b/apps/app/components/states/create-update-state-inline.tsx @@ -17,7 +17,7 @@ import useToast from "hooks/use-toast"; // ui import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui"; // types -import type { IState, IStateResponse } from "types"; +import type { ICurrentUserResponse, IState, IStateResponse } from "types"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; // constants @@ -27,6 +27,7 @@ type Props = { data: IState | null; onClose: () => void; selectedGroup: StateGroup | null; + user: ICurrentUserResponse | undefined; }; export type StateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled" | null; @@ -37,7 +38,12 @@ const defaultValues: Partial = { group: "backlog", }; -export const CreateUpdateStateInline: React.FC = ({ data, onClose, selectedGroup }) => { +export const CreateUpdateStateInline: React.FC = ({ + data, + onClose, + selectedGroup, + user, +}) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -83,7 +89,7 @@ export const CreateUpdateStateInline: React.FC = ({ data, onClose, select if (!data) { await stateService - .createState(workspaceSlug.toString(), projectId.toString(), { ...payload }) + .createState(workspaceSlug.toString(), projectId.toString(), { ...payload }, user) .then((res) => { mutate( STATES_LIST(projectId.toString()), @@ -121,9 +127,15 @@ export const CreateUpdateStateInline: React.FC = ({ data, onClose, select }); } else { await stateService - .updateState(workspaceSlug.toString(), projectId.toString(), data.id, { - ...payload, - }) + .updateState( + workspaceSlug.toString(), + projectId.toString(), + data.id, + { + ...payload, + }, + user + ) .then(() => { mutate(STATES_LIST(projectId.toString())); handleClose(); diff --git a/apps/app/components/states/delete-state-modal.tsx b/apps/app/components/states/delete-state-modal.tsx index 7fdce5d44..a92f235c4 100644 --- a/apps/app/components/states/delete-state-modal.tsx +++ b/apps/app/components/states/delete-state-modal.tsx @@ -15,7 +15,7 @@ import useToast from "hooks/use-toast"; // ui import { DangerButton, SecondaryButton } from "components/ui"; // types -import type { IState, IStateResponse } from "types"; +import type { ICurrentUserResponse, IState, IStateResponse } from "types"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; onClose: () => void; data: IState | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteStateModal: React.FC = ({ isOpen, onClose, data }) => { +export const DeleteStateModal: React.FC = ({ isOpen, onClose, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -44,7 +45,7 @@ export const DeleteStateModal: React.FC = ({ isOpen, onClose, data }) => setIsDeleteLoading(true); await stateServices - .deleteState(workspaceSlug as string, data.project, data.id) + .deleteState(workspaceSlug as string, data.project, data.id, user) .then(() => { mutate( STATES_LIST(data.project), diff --git a/apps/app/components/states/single-state.tsx b/apps/app/components/states/single-state.tsx index d9425c524..8bf4198fe 100644 --- a/apps/app/components/states/single-state.tsx +++ b/apps/app/components/states/single-state.tsx @@ -21,7 +21,7 @@ import { addSpaceIfCamelCase } from "helpers/string.helper"; import { groupBy, orderArrayBy } from "helpers/array.helper"; import { orderStateGroups } from "helpers/state.helper"; // types -import { IState } from "types"; +import { ICurrentUserResponse, IState } from "types"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; @@ -31,6 +31,7 @@ type Props = { statesList: IState[]; handleEditState: () => void; handleDeleteState: () => void; + user: ICurrentUserResponse | undefined; }; export const SingleState: React.FC = ({ @@ -39,6 +40,7 @@ export const SingleState: React.FC = ({ statesList, handleEditState, handleDeleteState, + user, }) => { const [isSubmitting, setIsSubmitting] = useState(false); @@ -67,14 +69,26 @@ export const SingleState: React.FC = ({ if (currentDefaultState) stateService - .patchState(workspaceSlug as string, projectId as string, currentDefaultState?.id ?? "", { - default: false, - }) + .patchState( + workspaceSlug as string, + projectId as string, + currentDefaultState?.id ?? "", + { + default: false, + }, + user + ) .then(() => { stateService - .patchState(workspaceSlug as string, projectId as string, state.id, { - default: true, - }) + .patchState( + workspaceSlug as string, + projectId as string, + state.id, + { + default: true, + }, + user + ) .then(() => { mutate(STATES_LIST(projectId as string)); setIsSubmitting(false); @@ -85,9 +99,15 @@ export const SingleState: React.FC = ({ }); else stateService - .patchState(workspaceSlug as string, projectId as string, state.id, { - default: true, - }) + .patchState( + workspaceSlug as string, + projectId as string, + state.id, + { + default: true, + }, + user + ) .then(() => { mutate(STATES_LIST(projectId as string)); setIsSubmitting(false); @@ -121,9 +141,15 @@ export const SingleState: React.FC = ({ ); stateService - .patchState(workspaceSlug as string, projectId as string, state.id, { - sequence: newSequence, - }) + .patchState( + workspaceSlug as string, + projectId as string, + state.id, + { + sequence: newSequence, + }, + user + ) .then((res) => { console.log(res); mutate(STATES_LIST(projectId as string)); diff --git a/apps/app/components/ui/avatar.tsx b/apps/app/components/ui/avatar.tsx index 7d2d9a575..06534c121 100644 --- a/apps/app/components/ui/avatar.tsx +++ b/apps/app/components/ui/avatar.tsx @@ -44,11 +44,9 @@ export const Avatar: React.FC = ({ width: width, }} > - {user.first_name}
diff --git a/apps/app/components/ui/icon.tsx b/apps/app/components/ui/icon.tsx new file mode 100644 index 000000000..aefc24dff --- /dev/null +++ b/apps/app/components/ui/icon.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +type Props = { + iconName: string; + className?: string; +}; + +export const Icon: React.FC = ({ iconName, className = "" }) => ( + + {iconName} + +); diff --git a/apps/app/components/ui/index.ts b/apps/app/components/ui/index.ts index 476f0af3e..6eb273c4a 100644 --- a/apps/app/components/ui/index.ts +++ b/apps/app/components/ui/index.ts @@ -25,3 +25,4 @@ export * from "./markdown-to-component"; export * from "./product-updates-modal"; export * from "./integration-and-import-export-banner"; export * from "./range-datepicker"; +export * from "./icon"; diff --git a/apps/app/components/views/delete-view-modal.tsx b/apps/app/components/views/delete-view-modal.tsx index 31cfc33e9..c57c29dc3 100644 --- a/apps/app/components/views/delete-view-modal.tsx +++ b/apps/app/components/views/delete-view-modal.tsx @@ -15,7 +15,7 @@ import { DangerButton, SecondaryButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import type { IView } from "types"; +import type { ICurrentUserResponse, IView } from "types"; // fetch-keys import { VIEWS_LIST } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; data: IView | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen }) => { +export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -43,7 +44,7 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen }) => if (!workspaceSlug || !data || !projectId) return; await viewsService - .deleteView(workspaceSlug as string, projectId as string, data.id) + .deleteView(workspaceSlug as string, projectId as string, data.id, user) .then(() => { mutate(VIEWS_LIST(projectId as string), (views) => views?.filter((view) => view.id !== data.id) diff --git a/apps/app/components/views/gantt-chart.tsx b/apps/app/components/views/gantt-chart.tsx index dc81f70fa..a445331b2 100644 --- a/apps/app/components/views/gantt-chart.tsx +++ b/apps/app/components/views/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view"; @@ -38,9 +40,23 @@ export const ViewIssuesGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )}
); @@ -59,10 +75,20 @@ export const ViewIssuesGanttChartView: FC = ({}) => { const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/views/modal.tsx b/apps/app/components/views/modal.tsx index a04c19b64..755251356 100644 --- a/apps/app/components/views/modal.tsx +++ b/apps/app/components/views/modal.tsx @@ -13,7 +13,7 @@ import useToast from "hooks/use-toast"; // components import { ViewForm } from "components/views"; // types -import { IView } from "types"; +import { ICurrentUserResponse, IView } from "types"; // fetch-keys import { VIEWS_LIST } from "constants/fetch-keys"; @@ -22,6 +22,7 @@ type Props = { handleClose: () => void; data?: IView | null; preLoadedData?: Partial | null; + user: ICurrentUserResponse | undefined; }; export const CreateUpdateViewModal: React.FC = ({ @@ -29,6 +30,7 @@ export const CreateUpdateViewModal: React.FC = ({ handleClose, data, preLoadedData, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -45,7 +47,7 @@ export const CreateUpdateViewModal: React.FC = ({ query_data: payload.query, }; await viewsService - .createView(workspaceSlug as string, projectId as string, payload) + .createView(workspaceSlug as string, projectId as string, payload, user) .then(() => { mutate(VIEWS_LIST(projectId as string)); handleClose(); @@ -71,7 +73,7 @@ export const CreateUpdateViewModal: React.FC = ({ query_data: payload.query, }; await viewsService - .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData) + .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user) .then((res) => { mutate( VIEWS_LIST(projectId as string), diff --git a/apps/app/components/workspace/create-workspace-form.tsx b/apps/app/components/workspace/create-workspace-form.tsx index 0b2b7d984..fdc078cf0 100644 --- a/apps/app/components/workspace/create-workspace-form.tsx +++ b/apps/app/components/workspace/create-workspace-form.tsx @@ -11,7 +11,7 @@ import useToast from "hooks/use-toast"; // ui import { CustomSelect, Input, PrimaryButton } from "components/ui"; // types -import { IWorkspace } from "types"; +import { ICurrentUserResponse, IWorkspace } from "types"; // fetch-keys import { USER_WORKSPACES } from "constants/fetch-keys"; // constants @@ -25,6 +25,7 @@ type Props = { company_size: number | null; }; setDefaultValues: Dispatch>; + user: ICurrentUserResponse | undefined; }; const restrictedUrls = [ @@ -35,6 +36,7 @@ const restrictedUrls = [ "invitations", "magic-sign-in", "onboarding", + "reset-password", "signin", "workspace-member-invitation", "404", @@ -44,6 +46,7 @@ export const CreateWorkspaceForm: React.FC = ({ onSubmit, defaultValues, setDefaultValues, + user, }) => { const [slugError, setSlugError] = useState(false); const [invalidSlug, setInvalidSlug] = useState(false); @@ -66,7 +69,7 @@ export const CreateWorkspaceForm: React.FC = ({ if (res.status === true && !restrictedUrls.includes(formData.slug)) { setSlugError(false); await workspaceService - .createWorkspace(formData) + .createWorkspace(formData, user) .then((res) => { setToastAlert({ type: "success", @@ -99,110 +102,105 @@ export const CreateWorkspaceForm: React.FC = ({ ); return ( - -
-
-
-
- Workspace name + +
+
+
+ Workspace name + + setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-")) + } + validations={{ + required: "Workspace name is required", + validate: (value) => + /^[\w\s-]*$/.test(value) || + `Name can only contain (" "), ( - ), ( _ ) & Alphanumeric characters.`, + }} + placeholder="e.g. My Workspace" + className="placeholder:text-brand-secondary" + error={errors.name} + /> +
+
+ Workspace URL +
+ + {typeof window !== "undefined" && window.location.origin}/ + - setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-")) - } + name="slug" + register={register} + className="block w-full rounded-md bg-transparent py-2 !px-0 text-sm" validations={{ - required: "Workspace name is required", - validate: (value) => - /^[\w\s-]*$/.test(value) || - `Name can only contain (" "), ( - ), ( _ ) & Alphanumeric characters.`, + required: "Workspace URL is required", }} - placeholder="e.g. My Workspace" - className="placeholder:text-brand-secondary" - error={errors.name} + onChange={(e) => + /^[a-zA-Z0-9_-]+$/.test(e.target.value) + ? setInvalidSlug(false) + : setInvalidSlug(true) + } />
-
- Workspace URL -
- - {typeof window !== "undefined" && window.location.origin}/ - - - /^[a-zA-Z0-9_-]+$/.test(e.target.value) - ? setInvalidSlug(false) - : setInvalidSlug(true) - } - /> -
- {slugError && ( - Workspace URL is already taken! - )} - {invalidSlug && ( - {`URL can only contain ( - ), ( _ ) & Alphanumeric characters.`} - )} -
-
- -
- How large is your company? -
- ( - Select company size - ) - } - input - width="w-full" - > - {COMPANY_SIZE?.map((item) => ( - - {item.label} - - ))} - - )} - /> - {errors.company_size && ( - {errors.company_size.message} - )} -
-
- -
- - {isSubmitting ? "Creating..." : "Create Workspace"} - + {slugError && ( + Workspace URL is already taken! + )} + {invalidSlug && ( + {`URL can only contain ( - ), ( _ ) & Alphanumeric characters.`} + )}
+ +
+ How large is your company? +
+ ( + Select company size + ) + } + input + width="w-full" + > + {COMPANY_SIZE?.map((item) => ( + + {item.label} + + ))} + + )} + /> + {errors.company_size && ( + {errors.company_size.message} + )} +
+
+
+ +
+ + {isSubmitting ? "Creating..." : "Create Workspace"} +
); diff --git a/apps/app/components/workspace/delete-workspace-modal.tsx b/apps/app/components/workspace/delete-workspace-modal.tsx index ceacd11e7..344d700b0 100644 --- a/apps/app/components/workspace/delete-workspace-modal.tsx +++ b/apps/app/components/workspace/delete-workspace-modal.tsx @@ -15,7 +15,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui import { DangerButton, Input, SecondaryButton } from "components/ui"; // types -import type { IWorkspace } from "types"; +import type { ICurrentUserResponse, IWorkspace } from "types"; // fetch-keys import { USER_WORKSPACES } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; data: IWorkspace | null; onClose: () => void; + user: ICurrentUserResponse | undefined; }; -export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose }) => { +export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [confirmWorkspaceName, setConfirmWorkspaceName] = useState(""); @@ -57,7 +58,7 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose }) setIsDeleteLoading(true); if (!data || !canDelete) return; await workspaceService - .deleteWorkspace(data.slug) + .deleteWorkspace(data.slug, user) .then(() => { handleClose(); router.push("/"); diff --git a/apps/app/components/workspace/send-workspace-invitation-modal.tsx b/apps/app/components/workspace/send-workspace-invitation-modal.tsx index f58269b89..52dc74149 100644 --- a/apps/app/components/workspace/send-workspace-invitation-modal.tsx +++ b/apps/app/components/workspace/send-workspace-invitation-modal.tsx @@ -10,7 +10,7 @@ import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ // hooks import useToast from "hooks/use-toast"; // types -import { IWorkspaceMemberInvitation } from "types"; +import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types"; // fetch keys import { WORKSPACE_INVITATIONS } from "constants/fetch-keys"; // constants @@ -21,6 +21,7 @@ type Props = { setIsOpen: React.Dispatch>; workspace_slug: string; members: any[]; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { @@ -33,6 +34,7 @@ const SendWorkspaceInvitationModal: React.FC = ({ setIsOpen, workspace_slug, members, + user, }) => { const { setToastAlert } = useToast(); @@ -54,7 +56,7 @@ const SendWorkspaceInvitationModal: React.FC = ({ const onSubmit = async (formData: IWorkspaceMemberInvitation) => { await workspaceService - .inviteWorkspace(workspace_slug, { emails: [formData] }) + .inviteWorkspace(workspace_slug, { emails: [formData] }, user) .then((res) => { setIsOpen(false); handleClose(); @@ -101,7 +103,10 @@ const SendWorkspaceInvitationModal: React.FC = ({
- + Members

diff --git a/apps/app/components/workspace/sidebar-dropdown.tsx b/apps/app/components/workspace/sidebar-dropdown.tsx index e4d3db7c5..6e5774434 100644 --- a/apps/app/components/workspace/sidebar-dropdown.tsx +++ b/apps/app/components/workspace/sidebar-dropdown.tsx @@ -1,9 +1,10 @@ import { Fragment } from "react"; -import { Menu, Transition } from "@headlessui/react"; + import { useRouter } from "next/router"; -import Image from "next/image"; import Link from "next/link"; -import { CheckIcon, PlusIcon } from "@heroicons/react/24/outline"; + +// headless ui +import { Menu, Transition } from "@headlessui/react"; // hooks import useUser from "hooks/use-user"; import useTheme from "hooks/use-theme"; @@ -14,7 +15,9 @@ import userService from "services/user.service"; import authenticationService from "services/authentication.service"; // components import { Avatar, Loader } from "components/ui"; -// helper +// icons +import { CheckIcon, PlusIcon } from "@heroicons/react/24/outline"; +// helpers import { truncateText } from "helpers/string.helper"; // types import { IWorkspace } from "types"; @@ -67,17 +70,19 @@ export const WorkspaceSidebarDropdown = () => { }; const handleSignOut = async () => { - router.push("/signin").then(() => { - mutateUser(); - }); - - await authenticationService.signOut().catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Failed to sign out. Please try again.", + await authenticationService + .signOut() + .then(() => { + mutateUser(undefined); + router.push("/"); }) - ); + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Failed to sign out. Please try again.", + }) + ); }; return ( @@ -92,12 +97,10 @@ export const WorkspaceSidebarDropdown = () => { >

{activeWorkspace?.logo && activeWorkspace.logo !== "" ? ( - Workspace Logo ) : ( activeWorkspace?.name?.charAt(0) ?? "..." @@ -137,8 +140,8 @@ export const WorkspaceSidebarDropdown = () => { border border-brand-base bg-brand-surface-2 shadow-lg focus:outline-none" >
-
{user?.email}
- Workspace +
{user?.email}
+ Workspace {workspaces ? (
{workspaces.length > 0 ? ( @@ -153,12 +156,10 @@ export const WorkspaceSidebarDropdown = () => {
{workspace?.logo && workspace.logo !== "" ? ( - Workspace Logo ) : ( workspace?.name?.charAt(0) ?? "..." diff --git a/apps/app/components/workspace/single-invitation.tsx b/apps/app/components/workspace/single-invitation.tsx index e9092c11d..73f6d4655 100644 --- a/apps/app/components/workspace/single-invitation.tsx +++ b/apps/app/components/workspace/single-invitation.tsx @@ -1,7 +1,5 @@ -// next -import Image from "next/image"; -// react -import { useState } from "react"; +// helpers +import { getFirstCharacters, truncateText } from "helpers/string.helper"; // types import { IWorkspaceMemberInvitation } from "types"; @@ -15,63 +13,57 @@ const SingleInvitation: React.FC = ({ invitation, invitationsRespond, handleInvitation, -}) => { - const [isChecked, setIsChecked] = useState(invitationsRespond.includes(invitation.id)); - - return ( - <> -
  • -
  • + -
  • - - ); -}; + ) : ( + + {getFirstCharacters(invitation.workspace.name)} + + )} +
    +
    +
    +
    {truncateText(invitation.workspace.name, 30)}
    +

    + Invited by{" "} + {invitation.created_by_detail + ? invitation.created_by_detail.first_name + : invitation.workspace.owner.first_name} +

    +
    +
    + +
    + + +); export default SingleInvitation; diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 044dad38b..75b187b2b 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -71,9 +71,18 @@ export const PROJECT_ISSUE_LABELS = (projectId: string) => export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => `PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`; -export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`; -export const CYCLE_INCOMPLETE_LIST = (projectId: string) => - `CYCLE_INCOMPLETE_LIST_${projectId.toUpperCase()}`; +// cycles +export const CYCLES_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`; +export const INCOMPLETE_CYCLES_LIST = (projectId: string) => + `INCOMPLETE_CYCLES_LIST_${projectId.toUpperCase()}`; +export const CURRENT_CYCLE_LIST = (projectId: string) => + `CURRENT_CYCLE_LIST_${projectId.toUpperCase()}`; +export const UPCOMING_CYCLES_LIST = (projectId: string) => + `UPCOMING_CYCLES_LIST_${projectId.toUpperCase()}`; +export const DRAFT_CYCLES_LIST = (projectId: string) => + `DRAFT_CYCLES_LIST_${projectId.toUpperCase()}`; +export const COMPLETED_CYCLES_LIST = (projectId: string) => + `COMPLETED_CYCLES_LIST_${projectId.toUpperCase()}`; export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId.toUpperCase()}`; export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { if (!params) return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}`; @@ -83,12 +92,6 @@ export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}_${paramsKey.toUpperCase()}`; }; export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`; -export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) => - `CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId.toUpperCase()}`; -export const CYCLE_DRAFT_LIST = (projectId: string) => - `CYCLE_DRAFT_LIST_${projectId.toUpperCase()}`; -export const CYCLE_COMPLETE_LIST = (projectId: string) => - `CYCLE_COMPLETE_LIST_${projectId.toUpperCase()}`; export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`; export const STATE_DETAILS = "STATE_DETAILS"; diff --git a/apps/app/constants/issue.ts b/apps/app/constants/issue.ts index 9f5020d04..0a3d37809 100644 --- a/apps/app/constants/issue.ts +++ b/apps/app/constants/issue.ts @@ -15,7 +15,7 @@ export const ORDER_BY_OPTIONS: Array<{ }> = [ { name: "Manual", key: "sort_order" }, { name: "Last created", key: "-created_at" }, - { name: "Last updated", key: "updated_at" }, + { name: "Last updated", key: "-updated_at" }, { name: "Priority", key: "priority" }, ]; diff --git a/apps/app/constants/seo-variables.ts b/apps/app/constants/seo-variables.ts index 2b2d876d2..aafd5f7a3 100644 --- a/apps/app/constants/seo-variables.ts +++ b/apps/app/constants/seo-variables.ts @@ -1,8 +1,8 @@ -export const SITE_NAME = "Plane"; -export const SITE_TITLE = "Plane | Accelerate software development with peace."; +export const SITE_NAME = "Plane | Simple, extensible, open-source project management tool."; +export const SITE_TITLE = "Plane | Simple, extensible, open-source project management tool."; export const SITE_DESCRIPTION = - "Plane accelerated the software development by order of magnitude for agencies and product companies."; + "Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind."; export const SITE_KEYWORDS = - "software development, plan, ship, software, accelerate, code management, release management"; -export const SITE_URL = "http://localhost:3000/"; -export const TWITTER_USER_NAME = "caravel"; + "software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration"; +export const SITE_URL = "https://app.plane.so/"; +export const TWITTER_USER_NAME = "Plane | Simple, extensible, open-source project management tool."; diff --git a/apps/app/constants/workspace.ts b/apps/app/constants/workspace.ts index 24f886af9..8ab10c09d 100644 --- a/apps/app/constants/workspace.ts +++ b/apps/app/constants/workspace.ts @@ -43,27 +43,27 @@ export const ONBOARDING_CARDS = { step: "2/5", title: "Plan with Issues", description: - "The issue is the building block of the Plane. Most concepts in Plane are either associated with issues and their properties.", + "Issues are the building blocks of Plane. Most concepts in Plane are associated with issues or their properties.", }, cycle: { imgURL: Cycle, step: "3/5", title: "Move with Cycles", description: - "Cycles help you and your team to progress faster, similar to the sprints commonly used in agile development.", + "Cycles help you and your team progress faster, similar to sprints commonly used in agile development.", }, module: { imgURL: Module, step: "4/5", title: "Break into Modules ", description: - "Modules break your big think into Projects or Features, to help you organize better.", + "Modules break your big thoughts into Projects or Features, to help you organize better.", }, commandMenu: { imgURL: CommandMenu, step: "5 /5", title: "Command Menu", - description: "With Command Menu, you can create, update and navigate across the platform.", + description: "With Command Menu, you can create, update, and navigate across the platform.", }, }; diff --git a/apps/app/contexts/issue-view.context.tsx b/apps/app/contexts/issue-view.context.tsx index ce24f5048..619027866 100644 --- a/apps/app/contexts/issue-view.context.tsx +++ b/apps/app/contexts/issue-view.context.tsx @@ -18,6 +18,7 @@ import { IProjectMember, TIssueGroupByOptions, TIssueOrderByOptions, + ICurrentUserResponse, } from "types"; // fetch-keys import { @@ -26,6 +27,7 @@ import { USER_PROJECT_VIEW, VIEW_DETAILS, } from "constants/fetch-keys"; +import useUserAuth from "hooks/use-user-auth"; export const issueViewContext = createContext({} as ContextType); @@ -190,6 +192,19 @@ export const reducer: ReducerFunctionType = (state, action) => { }; const saveDataToServer = async (workspaceSlug: string, projectID: string, state: any) => { + mutate( + workspaceSlug && projectID ? USER_PROJECT_VIEW(projectID as string) : null, + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + view_props: state, + }; + }, + false + ); + await projectService.setProjectView(workspaceSlug, projectID, { view_props: state, }); @@ -199,33 +214,54 @@ const saveCycleFilters = async ( workspaceSlug: string, projectId: string, cycleId: string, - state: any + state: any, + user: ICurrentUserResponse | undefined ) => { - await cyclesService.patchCycle(workspaceSlug, projectId, cycleId, { - ...state, - }); + await cyclesService.patchCycle( + workspaceSlug, + projectId, + cycleId, + { + ...state, + }, + user + ); }; const saveModuleFilters = async ( workspaceSlug: string, projectId: string, moduleId: string, - state: any + state: any, + user: ICurrentUserResponse | undefined ) => { - await modulesService.patchModule(workspaceSlug, projectId, moduleId, { - ...state, - }); + await modulesService.patchModule( + workspaceSlug, + projectId, + moduleId, + { + ...state, + }, + user + ); }; const saveViewFilters = async ( workspaceSlug: string, projectId: string, viewId: string, - state: any + state: any, + user: ICurrentUserResponse | undefined ) => { - await viewsService.patchView(workspaceSlug, projectId, viewId, { - ...state, - }); + await viewsService.patchView( + workspaceSlug, + projectId, + viewId, + { + ...state, + }, + user + ); }; const setNewDefault = async (workspaceSlug: string, projectId: string, state: any) => { @@ -254,6 +290,8 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; + const { user } = useUserAuth(); + const { data: myViewProps, mutate: mutateMyViewProps } = useSWR( workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null, workspaceSlug && projectId @@ -492,14 +530,20 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = }; }, false); - saveCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), { - view_props: { - filters: { - ...state.filters, - ...property, + saveCycleFilters( + workspaceSlug.toString(), + projectId.toString(), + cycleId.toString(), + { + view_props: { + filters: { + ...state.filters, + ...property, + }, }, }, - }); + user + ); } else if (moduleId) { mutateModuleDetails((prevData: any) => { if (!prevData) return prevData; @@ -515,14 +559,20 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = }; }, false); - saveModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), { - view_props: { - filters: { - ...state.filters, - ...property, + saveModuleFilters( + workspaceSlug.toString(), + projectId.toString(), + moduleId.toString(), + { + view_props: { + filters: { + ...state.filters, + ...property, + }, }, }, - }); + user + ); } else if (viewId) { mutateViewDetails((prevData: any) => { if (!prevData) return prevData; @@ -535,12 +585,18 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = }; }, false); if (saveToServer) - saveViewFilters(workspaceSlug as string, projectId as string, viewId as string, { - query_data: { - ...state.filters, - ...property, + saveViewFilters( + workspaceSlug as string, + projectId as string, + viewId as string, + { + query_data: { + ...state.filters, + ...property, + }, }, - }); + user + ); } else { mutateMyViewProps((prevData) => { if (!prevData) return prevData; diff --git a/apps/app/contexts/theme.context.tsx b/apps/app/contexts/theme.context.tsx index 318f04095..e8842ee59 100644 --- a/apps/app/contexts/theme.context.tsx +++ b/apps/app/contexts/theme.context.tsx @@ -5,7 +5,7 @@ import useSWR from "swr"; // components import ToastAlert from "components/toast-alert"; // hooks -import useUser from "hooks/use-user"; +import useUserAuth from "hooks/use-user-auth"; // services import projectService from "services/project.service"; // fetch-keys @@ -65,7 +65,7 @@ export const reducer: ReducerFunctionType = (state, action) => { export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); - const { user } = useUser(); + const { user } = useUserAuth(null); const router = useRouter(); const { workspaceSlug, projectId } = router.query; diff --git a/apps/app/helpers/array.helper.ts b/apps/app/helpers/array.helper.ts index 3cd326be7..2432f88ad 100644 --- a/apps/app/helpers/array.helper.ts +++ b/apps/app/helpers/array.helper.ts @@ -8,10 +8,12 @@ export const groupBy = (array: any[], key: string) => { }; export const orderArrayBy = ( - array: any[], + orgArray: any[], key: string, ordering: "ascending" | "descending" = "ascending" ) => { + const array = [...orgArray]; + if (!array || !Array.isArray(array) || array.length === 0) return []; if (key[0] === "-") { diff --git a/apps/app/helpers/string.helper.ts b/apps/app/helpers/string.helper.ts index a13f149fc..4dfc9855b 100644 --- a/apps/app/helpers/string.helper.ts +++ b/apps/app/helpers/string.helper.ts @@ -109,3 +109,12 @@ export const generateRandomColor = (string: string): string => { return randomColor; }; + +export const getFirstCharacters = (str: string) => { + const words = str.trim().split(" "); + if (words.length === 1) { + return words[0].charAt(0); + } else { + return words[0].charAt(0) + words[1].charAt(0); + } +}; diff --git a/apps/app/hooks/use-my-issues-filter.tsx b/apps/app/hooks/use-my-issues-filter.tsx index bd97427f5..8c5d3eaa5 100644 --- a/apps/app/hooks/use-my-issues-filter.tsx +++ b/apps/app/hooks/use-my-issues-filter.tsx @@ -1,26 +1,18 @@ -import { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; +import { useState, useEffect, useCallback } from "react"; +import useSWR, { mutate } from "swr"; // services -import stateService from "services/state.service"; -import userService from "services/user.service"; +import workspaceService from "services/workspace.service"; // hooks import useUser from "hooks/use-user"; -// helpers -import { groupBy } from "helpers/array.helper"; -import { getStatesList } from "helpers/state.helper"; // types -import { Properties, NestedKeyOf, IIssue } from "types"; -// fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; -// constants -import { PRIORITIES } from "constants/project"; +import { IWorkspaceMember, Properties } from "types"; +import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; const initialValues: Properties = { assignee: true, due_date: false, key: true, - labels: true, + labels: false, priority: false, state: true, sub_issue_count: false, @@ -29,99 +21,80 @@ const initialValues: Properties = { estimate: false, }; -// TODO: Refactor this logic -const useMyIssuesProperties = (issues?: IIssue[]) => { +const useMyIssuesProperties = (workspaceSlug?: string) => { const [properties, setProperties] = useState(initialValues); - const [groupByProperty, setGroupByProperty] = useState | null>(null); - - // FIXME: where this hook is used we may not have project id in the url - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; const { user } = useUser(); - const { data: stateGroups } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => stateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - const states = getStatesList(stateGroups ?? {}); - - useEffect(() => { - if (!user) return; - setProperties({ ...initialValues, ...user.my_issues_prop?.properties }); - setGroupByProperty(user.my_issues_prop?.groupBy ?? null); - }, [user]); - - const groupedByIssues: { - [key: string]: IIssue[]; - } = { - ...(groupByProperty === "state_detail.name" - ? Object.fromEntries( - states - ?.sort((a, b) => a.sequence - b.sequence) - ?.map((state) => [ - state.name, - issues?.filter((issue) => issue.state === state.name) ?? [], - ]) ?? [] - ) - : groupByProperty === "priority" - ? Object.fromEntries( - PRIORITIES.map((priority) => [ - priority, - issues?.filter((issue) => issue.priority === priority) ?? [], - ]) - ) - : {}), - ...groupBy(issues ?? [], groupByProperty ?? ""), - }; - - const setMyIssueProperty = (key: keyof Properties) => { - if (!user) return; - userService.updateUser({ my_issues_prop: { properties, groupBy: groupByProperty } }); - setProperties((prevData) => ({ - ...prevData, - [key]: !prevData[key], - })); - localStorage.setItem( - "my_issues_prop", - JSON.stringify({ - properties: { - ...properties, - [key]: !properties[key], - }, - groupBy: groupByProperty, - }) - ); - }; - - const setMyIssueGroupByProperty = (groupByProperty: NestedKeyOf | null) => { - if (!user) return; - userService.updateUser({ my_issues_prop: { properties, groupBy: groupByProperty } }); - setGroupByProperty(groupByProperty); - localStorage.setItem( - "my_issues_prop", - JSON.stringify({ properties, groupBy: groupByProperty }) - ); - }; - - useEffect(() => { - const viewProps = localStorage.getItem("my_issues_prop"); - if (viewProps) { - const { properties, groupBy } = JSON.parse(viewProps); - setProperties(properties); - setGroupByProperty(groupBy); + const { data: myWorkspace } = useSWR( + workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null, + workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug as string) : null, + { + shouldRetryOnError: false, } - }, []); + ); - return { - filteredIssues: groupedByIssues, - groupByProperty, - properties, - setMyIssueProperty, - setMyIssueGroupByProperty, - } as const; + useEffect(() => { + if (!myWorkspace || !workspaceSlug || !user) return; + + setProperties({ ...initialValues, ...myWorkspace.view_props }); + + if (!myWorkspace.view_props) { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { ...initialValues }, + }); + } + }, [myWorkspace, workspaceSlug, user]); + + const updateIssueProperties = useCallback( + (key: keyof Properties) => { + if (!workspaceSlug || !user) return; + + setProperties((prev) => ({ ...prev, [key]: !prev[key] })); + + if (myWorkspace) { + mutate( + WORKSPACE_MEMBERS_ME(workspaceSlug.toString()), + (prevData) => { + if (!prevData) return; + return { + ...prevData, + view_props: { ...prevData?.view_props, [key]: !prevData.view_props?.[key] }, + }; + }, + false + ); + if (myWorkspace.view_props) { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { + ...myWorkspace.view_props, + [key]: !myWorkspace.view_props[key], + }, + }); + } else { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { ...initialValues }, + }); + } + } + }, + [workspaceSlug, myWorkspace, user] + ); + + const newProperties: Properties = { + assignee: properties.assignee, + due_date: properties.due_date, + key: properties.key, + labels: properties.labels, + priority: properties.priority, + state: properties.state, + sub_issue_count: properties.sub_issue_count, + attachment_count: properties.attachment_count, + link: properties.link, + estimate: properties.estimate, + }; + + return [newProperties, updateIssueProperties] as const; }; export default useMyIssuesProperties; diff --git a/apps/app/hooks/use-projects.tsx b/apps/app/hooks/use-projects.tsx index 247fc0db6..a8a2d455b 100644 --- a/apps/app/hooks/use-projects.tsx +++ b/apps/app/hooks/use-projects.tsx @@ -19,9 +19,9 @@ const useProjects = () => { workspaceSlug ? () => projectService.getProjects(workspaceSlug as string) : null ); - const recentProjects = projects + const recentProjects = [...(projects ?? [])] ?.sort((a, b) => Date.parse(`${a.updated_at}`) - Date.parse(`${b.updated_at}`)) - .filter((_item, index) => index < 3); + ?.slice(0, 3); return { projects: orderArrayBy(projects ?? [], "is_favorite", "descending") || [], diff --git a/apps/app/hooks/use-user-auth.tsx b/apps/app/hooks/use-user-auth.tsx new file mode 100644 index 000000000..3e92fd3fe --- /dev/null +++ b/apps/app/hooks/use-user-auth.tsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from "react"; +// next imports +import { useRouter } from "next/router"; +// swr +import useSWR from "swr"; +// keys +import { CURRENT_USER } from "constants/fetch-keys"; +// services +import userService from "services/user.service"; +import workspaceService from "services/workspace.service"; +// types +import type { IWorkspace, ICurrentUserResponse } from "types"; + +const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "admin") => { + const router = useRouter(); + const { next_url } = router.query as { next_url: string }; + + const [isRouteAccess, setIsRouteAccess] = useState(true); + + const { + data: user, + isLoading, + error, + mutate, + } = useSWR(CURRENT_USER, () => userService.currentUser(), { + refreshInterval: 0, + }); + + useEffect(() => { + const handleWorkSpaceRedirection = async () => { + workspaceService.userWorkspaces().then(async (userWorkspaces) => { + const lastActiveWorkspace = userWorkspaces.find( + (workspace: IWorkspace) => workspace.id === user?.last_workspace_id + ); + if (lastActiveWorkspace) { + router.push(`/${lastActiveWorkspace.slug}`); + return; + } else if (userWorkspaces.length > 0) { + router.push(`/${userWorkspaces[0].slug}`); + return; + } else { + const invitations = await workspaceService.userWorkspaceInvitations(); + if (invitations.length > 0) { + router.push(`/invitations`); + return; + } else { + router.push(`/create-workspace`); + return; + } + } + }); + }; + + const handleUserRouteAuthentication = async () => { + if (user && user.is_active) { + if (routeAuth === "sign-in") { + if (user.is_onboarded) handleWorkSpaceRedirection(); + else { + router.push("/onboarding"); + return; + } + } else if (routeAuth === "onboarding") { + if (user.is_onboarded) handleWorkSpaceRedirection(); + else { + setIsRouteAccess(() => false); + return; + } + } else { + if (!user.is_onboarded) { + router.push("/onboarding"); + return; + } else { + setIsRouteAccess(() => false); + return; + } + } + } else { + // user is not active and we can redirect to no access page + // router.push("/no-access"); + // remove token + return; + } + }; + + if (routeAuth === null) { + setIsRouteAccess(() => false); + return; + } else { + if (!isLoading) { + setIsRouteAccess(() => true); + if (user) { + if (next_url) router.push(next_url); + else handleUserRouteAuthentication(); + } else { + if (routeAuth === "sign-in") { + setIsRouteAccess(() => false); + return; + } else { + router.push("/"); + return; + } + } + } + } + }, [user, isLoading, routeAuth, router, next_url]); + + return { + isLoading: isRouteAccess, + user: error ? undefined : user, + mutateUser: mutate, + assignedIssuesLength: user?.assigned_issues ?? 0, + workspaceInvitesLength: user?.workspace_invites ?? 0, + }; +}; + +export default useUserAuth; diff --git a/apps/app/hooks/use-user.tsx b/apps/app/hooks/use-user.tsx index e59cb32e5..f5186a273 100644 --- a/apps/app/hooks/use-user.tsx +++ b/apps/app/hooks/use-user.tsx @@ -1,13 +1,55 @@ -import { useContext } from "react"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// services +import userService from "services/user.service"; +// constants +import { CURRENT_USER } from "constants/fetch-keys"; +// types +import type { ICurrentUserResponse, IUser } from "types"; -// context -import { UserContext } from "contexts/user.context"; +export default function useUser({ redirectTo = "", redirectIfFound = false, options = {} } = {}) { + const router = useRouter(); + // API to fetch user information + const { data, isLoading, error, mutate } = useSWR( + CURRENT_USER, + () => userService.currentUser(), + options + ); -const useUser = () => { - // context - const contextData = useContext(UserContext); + const user = error ? undefined : data; + // console.log("useUser", user); - return { ...contextData }; -}; + useEffect(() => { + // if no redirect needed, just return (example: already on /dashboard) + // if user data not yet there (fetch in progress, logged in or not) then don't do anything yet + if (!redirectTo || !user) return; -export default useUser; + if ( + // If redirectTo is set, redirect if the user was not found. + (redirectTo && !redirectIfFound) || + // If redirectIfFound is also set, redirect if the user was found + (redirectIfFound && user) + ) { + router.push(redirectTo); + return; + // const nextLocation = router.asPath.split("?next=")[1]; + // if (nextLocation) { + // router.push(nextLocation as string); + // return; + // } else { + // router.push("/"); + // return; + // } + } + }, [user, redirectIfFound, redirectTo, router]); + + return { + user, + isUserLoading: isLoading, + mutateUser: mutate, + userError: error, + assignedIssuesLength: user?.assigned_issues ?? 0, + workspaceInvitesLength: user?.workspace_invites ?? 0, + }; +} diff --git a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx index eec8a8f2e..e19ea6b70 100644 --- a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx @@ -5,10 +5,7 @@ import { useRouter } from "next/router"; // contexts import { useProjectMyMembership, ProjectMemberProvider } from "contexts/project-member.context"; -// hooks -import useIssuesView from "hooks/use-issues-view"; // layouts -import Container from "layouts/container"; import AppHeader from "layouts/app-layout/app-header"; import AppSidebar from "layouts/app-layout/app-sidebar"; // components @@ -19,15 +16,7 @@ import { PrimaryButton, Spinner } from "components/ui"; // icons import { LayerDiagonalIcon } from "components/icons"; -type Meta = { - title?: string | null; - description?: string | null; - image?: string | null; - url?: string | null; -}; - type Props = { - meta?: Meta; children: React.ReactNode; noHeader?: boolean; bg?: "primary" | "secondary"; @@ -43,7 +32,6 @@ export const ProjectAuthorizationWrapper: React.FC = (props) => ( ); const ProjectAuthorizationWrapped: React.FC = ({ - meta, children, noHeader = false, bg = "primary", @@ -61,7 +49,7 @@ const ProjectAuthorizationWrapped: React.FC = ({ const settingsLayout = router.pathname.includes("/settings"); return ( - + <>
    @@ -126,6 +114,6 @@ const ProjectAuthorizationWrapped: React.FC = ({ )}
    -
    + ); }; diff --git a/apps/app/layouts/auth-layout/user-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/user-authorization-wrapper.tsx index d11bb2ac1..f48403b0f 100644 --- a/apps/app/layouts/auth-layout/user-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/user-authorization-wrapper.tsx @@ -32,7 +32,7 @@ export const UserAuthorizationLayout: React.FC = ({ children }) => { if (error) { const redirectTo = router.asPath; - router.push(`/signin?next=${redirectTo}`); + router.push(`/?next=${redirectTo}`); return null; } diff --git a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx index 4bc5156b7..e67424758 100644 --- a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx @@ -8,7 +8,6 @@ import useSWR from "swr"; // services import workspaceServices from "services/workspace.service"; // layouts -import Container from "layouts/container"; import AppSidebar from "layouts/app-layout/app-sidebar"; import AppHeader from "layouts/app-layout/app-header"; import { UserAuthorizationLayout } from "./user-authorization-wrapper"; @@ -21,15 +20,7 @@ import { LayerDiagonalIcon } from "components/icons"; // fetch-keys import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; -type Meta = { - title?: string | null; - description?: string | null; - image?: string | null; - url?: string | null; -}; - type Props = { - meta?: Meta; children: React.ReactNode; noHeader?: boolean; bg?: "primary" | "secondary"; @@ -39,7 +30,6 @@ type Props = { }; export const WorkspaceAuthorizationLayout: React.FC = ({ - meta, children, noHeader = false, bg = "primary", @@ -88,50 +78,48 @@ export const WorkspaceAuthorizationLayout: React.FC = ({ return ( - - -
    - - {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( - - - - Go to workspace - - - - } - type="workspace" - /> - ) : ( -
    - {!noHeader && ( - - )} -
    -
    - {children} -
    + +
    + + {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( + + + + Go to workspace + + + + } + type="workspace" + /> + ) : ( +
    + {!noHeader && ( + + )} +
    +
    + {children}
    -
    - )} -
    - +
    +
    + )} +
    ); }; diff --git a/apps/app/layouts/container.tsx b/apps/app/layouts/container.tsx deleted file mode 100644 index 5adf9430b..000000000 --- a/apps/app/layouts/container.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from "react"; -// next -import Head from "next/head"; -import { useRouter } from "next/router"; -// constants -import { - SITE_NAME, - SITE_DESCRIPTION, - SITE_URL, - TWITTER_USER_NAME, - SITE_KEYWORDS, - SITE_TITLE, -} from "constants/seo-variables"; - -type Meta = { - title?: string | null; - description?: string | null; - image?: string | null; - url?: string | null; -}; - -type Props = { - meta?: Meta; - children: React.ReactNode; - noPadding?: boolean; - bg?: "primary" | "secondary"; - noHeader?: boolean; - breadcrumbs?: JSX.Element; - left?: JSX.Element; - right?: JSX.Element; -}; - -const Container = ({ meta, children }: Props) => { - const router = useRouter(); - const image = meta?.image || "/site-image.png"; - const title = meta?.title || SITE_TITLE; - const url = meta?.url || `${SITE_URL}${router.asPath}`; - const description = meta?.description || SITE_DESCRIPTION; - - return ( - <> - - {title} - - - - - - - - - - - - - - {image && ( - - )} - - {children} - - ); -}; - -export default Container; diff --git a/apps/app/layouts/default-layout/index.tsx b/apps/app/layouts/default-layout/index.tsx index b615ab548..cce155906 100644 --- a/apps/app/layouts/default-layout/index.tsx +++ b/apps/app/layouts/default-layout/index.tsx @@ -1,15 +1,4 @@ -// layouts -import Container from "layouts/container"; - -type Meta = { - title?: string | null; - description?: string | null; - image?: string | null; - url?: string | null; -}; - type Props = { - meta?: Meta; children: React.ReactNode; noPadding?: boolean; bg?: "primary" | "secondary"; @@ -19,12 +8,10 @@ type Props = { right?: JSX.Element; }; -const DefaultLayout: React.FC = ({ meta, children }) => ( - -
    - <>{children} -
    -
    +const DefaultLayout: React.FC = ({ children }) => ( +
    + <>{children} +
    ); export default DefaultLayout; diff --git a/apps/app/lib/auth.ts b/apps/app/lib/auth.ts index e607fd00c..47a52663d 100644 --- a/apps/app/lib/auth.ts +++ b/apps/app/lib/auth.ts @@ -104,7 +104,7 @@ export const homePageRedirect = async (cookie?: string) => { if (!user) return { redirect: { - destination: "/signin", + destination: "/", permanent: false, }, }; diff --git a/apps/app/next.config.js b/apps/app/next.config.js index 876694142..646504a54 100644 --- a/apps/app/next.config.js +++ b/apps/app/next.config.js @@ -16,6 +16,7 @@ const nextConfig = { "planefs.s3.amazonaws.com", "images.unsplash.com", "avatars.githubusercontent.com", + "localhost", ...extraImageDomains, ], }, diff --git a/apps/app/package.json b/apps/app/package.json index af90cf99a..01287f068 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -48,7 +48,7 @@ "react-markdown": "^8.0.7", "recharts": "^2.3.2", "remirror": "^2.0.23", - "swr": "^1.3.0", + "swr": "^2.1.3", "tlds": "^1.238.0", "uuid": "^9.0.0" }, diff --git a/apps/app/pages/404.tsx b/apps/app/pages/404.tsx index 2a136a164..a70bc2345 100644 --- a/apps/app/pages/404.tsx +++ b/apps/app/pages/404.tsx @@ -13,12 +13,7 @@ import Image404 from "public/404.svg"; import type { NextPage } from "next"; const PageNotFound: NextPage = () => ( - +
    diff --git a/apps/app/pages/[workspaceSlug]/analytics.tsx b/apps/app/pages/[workspaceSlug]/analytics.tsx index b32399448..eb2816b56 100644 --- a/apps/app/pages/[workspaceSlug]/analytics.tsx +++ b/apps/app/pages/[workspaceSlug]/analytics.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from "react"; +import React, { Fragment, useEffect } from "react"; import { useRouter } from "next/router"; @@ -6,10 +6,13 @@ import useSWR from "swr"; // react-hook-form import { useForm } from "react-hook-form"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // headless ui import { Tab } from "@headlessui/react"; // services import analyticsService from "services/analytics.service"; +import trackEventServices from "services/track-event.service"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; // components @@ -34,6 +37,8 @@ const Analytics = () => { const router = useRouter(); const { workspaceSlug } = router.query; + const { user } = useUserAuth(); + const { control, watch, setValue } = useForm({ defaultValues }); const params: IAnalyticsParams = { @@ -48,6 +53,29 @@ const Analytics = () => { workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null ); + const trackAnalyticsEvent = (tab: string) => { + const eventPayload = { + workspaceSlug: workspaceSlug?.toString(), + }; + + const eventType = + tab === "Scope and Demand" + ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" + : "WORKSPACE_CUSTOM_ANALYTICS"; + + trackEventServices.trackAnalyticsEvent(eventPayload, eventType, user); + }; + + useEffect(() => { + if (!workspaceSlug) return; + + trackEventServices.trackAnalyticsEvent( + { workspaceSlug: workspaceSlug?.toString() }, + "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS", + user + ); + }, [workspaceSlug]); + return ( { selected ? "bg-brand-surface-2" : "" }` } + onClick={() => trackAnalyticsEvent(tab)} > {tab} @@ -95,6 +124,7 @@ const Analytics = () => { params={params} control={control} setValue={setValue} + user={user} fullScreen /> diff --git a/apps/app/pages/[workspaceSlug]/index.tsx b/apps/app/pages/[workspaceSlug]/index.tsx index 6d8f3f9ed..233fd9b8a 100644 --- a/apps/app/pages/[workspaceSlug]/index.tsx +++ b/apps/app/pages/[workspaceSlug]/index.tsx @@ -41,10 +41,12 @@ const WorkspacePage: NextPage = () => { return ( - + {isProductUpdatesModalOpen && ( + + )}
    diff --git a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx index d742a7aa5..c66fcee55 100644 --- a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx +++ b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx @@ -14,7 +14,7 @@ import useIssues from "hooks/use-issues"; import { Spinner, EmptySpace, EmptySpaceItem, PrimaryButton } from "components/ui"; import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; // hooks -import useIssuesProperties from "hooks/use-issue-properties"; +import useMyIssuesProperties from "hooks/use-my-issues-filter"; // types import { IIssue, Properties } from "types"; // components @@ -31,10 +31,7 @@ const MyIssuesPage: NextPage = () => { // fetching user issues const { myIssues } = useIssues(workspaceSlug as string); - const [properties, setProperties] = useIssuesProperties( - workspaceSlug ? (workspaceSlug as string) : undefined, - undefined - ); + const [properties, setProperties] = useMyIssuesProperties(workspaceSlug as string); return ( { return ( diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx index b6c3d36b9..c64a5a4d1 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx @@ -1,14 +1,12 @@ import React, { useEffect, useState } from "react"; -import Image from "next/image"; - // react-hook-form import { Controller, useForm } from "react-hook-form"; // services import fileService from "services/file.service"; import userService from "services/user.service"; // hooks -import useUser from "hooks/use-user"; +import useUserAuth from "hooks/use-user-auth"; import useToast from "hooks/use-toast"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; @@ -50,7 +48,7 @@ const Profile: NextPage = () => { } = useForm({ defaultValues }); const { setToastAlert } = useToast(); - const { user: myProfile, mutateUser } = useUser(); + const { user: myProfile, mutateUser } = useUserAuth(); useEffect(() => { reset({ ...defaultValues, ...myProfile }); @@ -69,7 +67,7 @@ const Profile: NextPage = () => { .then((res) => { mutateUser((prevData) => { if (!prevData) return prevData; - return { ...prevData, user: { ...payload, ...res } }; + return { ...prevData, ...res }; }, false); setIsEditing(false); setToastAlert({ @@ -99,7 +97,7 @@ const Profile: NextPage = () => { if (updateUser) userService .updateUser({ avatar: "" }) - .then((res) => { + .then(() => { setToastAlert({ type: "success", title: "Success!", @@ -107,7 +105,7 @@ const Profile: NextPage = () => { }); mutateUser((prevData) => { if (!prevData) return prevData; - return { ...prevData, user: res }; + return { ...prevData, avatar: "" }; }, false); }) .catch(() => { @@ -123,9 +121,6 @@ const Profile: NextPage = () => { return ( @@ -171,14 +166,11 @@ const Profile: NextPage = () => {
    ) : (
    - {myProfile.first_name} setIsImageUploadModalOpen(true)} - priority + alt={myProfile.first_name} />
    )} diff --git a/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx index 5ea79014d..f2fe98f80 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTheme } from "next-themes"; // hooks -import useUser from "hooks/use-user"; +import useUserAuth from "hooks/use-user-auth"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import SettingsNavbar from "layouts/settings-navbar"; @@ -15,7 +15,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { ICustomTheme } from "types"; const ProfilePreferences = () => { - const { user: myProfile } = useUser(); + const { user: myProfile } = useUserAuth(); const { theme } = useTheme(); const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false); const [preLoadedData, setPreLoadedData] = useState(null); @@ -29,9 +29,6 @@ const ProfilePreferences = () => { return ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 4425014ac..718a8bd3a 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -19,6 +19,7 @@ import cycleServices from "services/cycles.service"; import projectService from "services/project.service"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // components import { AnalyticsProjectModal } from "components/analytics"; // ui @@ -30,7 +31,7 @@ import { getDateRangeStatus } from "helpers/date-time.helper"; // fetch-keys import { CYCLE_ISSUES, - CYCLE_LIST, + CYCLES_LIST, PROJECT_DETAILS, CYCLE_DETAILS, PROJECT_ISSUES_LIST, @@ -44,6 +45,8 @@ const SingleCycle: React.FC = () => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: activeProject } = useSWR( @@ -54,9 +57,9 @@ const SingleCycle: React.FC = () => { ); const { data: cycles } = useSWR( - workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null, + workspaceSlug && projectId ? CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cycleServices.getCycles(workspaceSlug as string, projectId as string) + ? () => cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, "all") : null ); @@ -94,7 +97,7 @@ const SingleCycle: React.FC = () => { if (!workspaceSlug || !projectId) return; await issuesService - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, data) + .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, data, user) .then(() => { mutate(CYCLE_ISSUES(cycleId as string)); }) @@ -185,6 +188,7 @@ const SingleCycle: React.FC = () => { cycle={cycleDetails} isOpen={cycleSidebar} isCompleted={cycleStatus === "completed" ?? false} + user={user} /> diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 377d20fae..2bd2aa1b9 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -3,72 +3,45 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; + +// headless ui +import { Tab } from "@headlessui/react"; // hooks +import useLocalStorage from "hooks/use-local-storage"; +import useUserAuth from "hooks/use-user-auth"; // services import cycleService from "services/cycles.service"; import projectService from "services/project.service"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // components -import { CreateUpdateCycleModal, CyclesView } from "components/cycles"; +import { + ActiveCycleDetails, + AllCyclesList, + CompletedCyclesList, + CreateUpdateCycleModal, + DraftCyclesList, + UpcomingCyclesList, +} from "components/cycles"; // ui import { PrimaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import { PlusIcon } from "@heroicons/react/24/outline"; +import { ListBulletIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; // types import { SelectCycleType } from "types"; import type { NextPage } from "next"; // fetch-keys -import { - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DRAFT_LIST, - PROJECT_DETAILS, - CYCLE_DETAILS, -} from "constants/fetch-keys"; +import { CURRENT_CYCLE_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; + +const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"]; const ProjectCycles: NextPage = () => { const [selectedCycle, setSelectedCycle] = useState(); const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false); - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { data: activeProject } = useSWR( - workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectService.getProject(workspaceSlug as string, projectId as string) - : null - ); - - const { data: draftCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => cycleService.getDraftCycles(workspaceSlug as string, projectId as string) - : null - ); - - const { data: currentAndUpcomingCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => cycleService.getCurrentAndUpcomingCycles(workspaceSlug as string, projectId as string) - : null - ); - - const { data: cyclesCompleteList } = useSWR( - workspaceSlug && projectId ? CYCLE_DETAILS(projectId as string) : null, - workspaceSlug && projectId - ? () => cycleService.getCycles(workspaceSlug as string, projectId as string) - : null - ); - - useEffect(() => { - if (createUpdateCycleModal) return; - const timer = setTimeout(() => { - setSelectedCycle(undefined); - clearTimeout(timer); - }, 500); - }, [createUpdateCycleModal]); + const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All"); + const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycleView", "list"); const currentTabValue = (tab: string | null) => { switch (tab) { @@ -87,11 +60,36 @@ const ProjectCycles: NextPage = () => { } }; + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { user } = useUserAuth(); + + const { data: activeProject } = useSWR( + workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, + workspaceSlug && projectId + ? () => projectService.getProject(workspaceSlug as string, projectId as string) + : null + ); + + const { data: currentCycle } = useSWR( + workspaceSlug && projectId ? CURRENT_CYCLE_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "current") + : null + ); + + useEffect(() => { + if (createUpdateCycleModal) return; + const timer = setTimeout(() => { + setSelectedCycle(undefined); + clearTimeout(timer); + }, 500); + }, [createUpdateCycleModal]); + return ( @@ -115,15 +113,118 @@ const ProjectCycles: NextPage = () => { isOpen={createUpdateCycleModal} handleClose={() => setCreateUpdateCycleModal(false)} data={selectedCycle} + user={user} />
    - +
    +

    Cycles

    +
    + + + +
    +
    + { + switch (i) { + case 0: + return setCycleTab("All"); + case 1: + return setCycleTab("Active"); + case 2: + return setCycleTab("Upcoming"); + case 3: + return setCycleTab("Completed"); + case 4: + return setCycleTab("Drafts"); + default: + return setCycleTab("All"); + } + }} + > + + {tabsList.map((tab, index) => { + if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts")) + return null; + + return ( + + `rounded-3xl border px-6 py-1 outline-none ${ + selected + ? "border-brand-accent bg-brand-accent text-white font-medium" + : "border-brand-base bg-brand-base hover:bg-brand-surface-2" + }` + } + > + {tab} + + ); + })} + + + + + + {cyclesView !== "gantt_chart" && ( + + {currentCycle?.[0] ? ( + + ) : ( +
    +

    + No active cycle is present. +

    +
    + )} +
    + )} + + + + + + + {cyclesView !== "gantt_chart" && ( + + + + )} +
    +
    ); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 15237617e..5f6615f55 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -7,6 +7,8 @@ import useSWR, { mutate } from "swr"; // react-hook-form import { useForm } from "react-hook-form"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // services import issuesService from "services/issues.service"; // layouts @@ -50,6 +52,8 @@ const IssueDetailsPage: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; + const { user } = useUserAuth(); + const { data: issueDetails, mutate: mutateIssueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId @@ -78,18 +82,21 @@ const IssueDetailsPage: NextPage = () => { async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...formData, + }; + }, false ); const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then((res) => { mutateIssueDetails(); mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); @@ -189,7 +196,7 @@ const IssueDetailsPage: NextPage = () => { ) : null}
    - +
    @@ -201,8 +208,8 @@ const IssueDetailsPage: NextPage = () => {

    Comments/Activity

    - - + +
    diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index 41e566d90..657f48fe2 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -17,6 +17,7 @@ import modulesService from "services/modules.service"; import issuesService from "services/issues.service"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // contexts @@ -49,6 +50,8 @@ const SingleModule: React.FC = () => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: issues } = useSWR( @@ -95,7 +98,13 @@ const SingleModule: React.FC = () => { if (!workspaceSlug || !projectId) return; await modulesService - .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, data) + .addIssuesToModule( + workspaceSlug as string, + projectId as string, + moduleId as string, + data, + user + ) .then(() => mutate(MODULE_ISSUES(moduleId as string))) .catch(() => setToastAlert({ @@ -172,52 +181,21 @@ const SingleModule: React.FC = () => { } > setAnalyticsModal(false)} /> - {moduleIssues ? ( - moduleIssues.length > 0 ? ( -
    - -
    - ) : ( -
    - - { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }} - /> - - -
    - ) - ) : ( -
    - -
    - )} + +
    + +
    + diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 3fa24e332..be92a9f86 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -6,6 +6,8 @@ import useSWR from "swr"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // services import projectService from "services/project.service"; import modulesService from "services/modules.service"; @@ -37,6 +39,8 @@ const ProjectModules: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { data: activeProject } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -66,9 +70,6 @@ const ProjectModules: NextPage = () => { return ( @@ -92,6 +93,7 @@ const ProjectModules: NextPage = () => { isOpen={createUpdateModule} setIsOpen={setCreateUpdateModule} data={selectedModule} + user={user} /> {modules ? ( modules.length > 0 ? ( @@ -129,6 +131,7 @@ const ProjectModules: NextPage = () => { key={module.id} module={module} handleEditModule={() => handleEditModule(module)} + user={user} /> ))}
    diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index d70559d3f..c157d7b96 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -116,7 +116,7 @@ const SinglePage: NextPage = () => { if (!formData.name || formData.name.length === 0 || formData.name === "") return; await pagesService - .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData) + .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData, user) .then(() => { mutate( PAGE_DETAILS(pageId as string), @@ -143,7 +143,7 @@ const SinglePage: NextPage = () => { ); await pagesService - .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData) + .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData, user) .then(() => { mutate(PAGE_DETAILS(pageId as string)); }); @@ -237,7 +237,8 @@ const SinglePage: NextPage = () => { result.draggableId, { sort_order: newSortOrder, - } + }, + user ); }; @@ -290,9 +291,6 @@ const SinglePage: NextPage = () => { return ( @@ -532,6 +530,7 @@ const SinglePage: NextPage = () => { block={block} projectDetails={projectDetails} index={index} + user={user} /> ))} {provided.placeholder} @@ -545,6 +544,7 @@ const SinglePage: NextPage = () => { setCreateBlockForm(false)} focus="name" + user={user} />
    )} @@ -553,6 +553,7 @@ const SinglePage: NextPage = () => { isOpen={labelModal} handleClose={() => setLabelModal(false)} projectId={projectId} + user={user} /> )} @@ -565,7 +566,7 @@ const SinglePage: NextPage = () => {
    - +
    ) : ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 874057e06..a51c8b44f 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -1,19 +1,15 @@ -import { useState } from "react"; +import { useState, Fragment } from "react"; import { useRouter } from "next/router"; import dynamic from "next/dynamic"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; -// react-hook-form -import { useForm } from "react-hook-form"; // headless ui import { Tab } from "@headlessui/react"; // services import projectService from "services/project.service"; -import pagesService from "services/pages.service"; // hooks -import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; // icons import { PlusIcon } from "components/icons"; @@ -22,20 +18,16 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // components import { RecentPagesList, CreateUpdatePageModal, TPagesListProps } from "components/pages"; // ui -import { Input, PrimaryButton } from "components/ui"; +import { PrimaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; // types -import { IPage, TPageViewProps } from "types"; +import { TPageViewProps } from "types"; import type { NextPage } from "next"; // fetch-keys -import { - ALL_PAGES_LIST, - MY_PAGES_LIST, - PROJECT_DETAILS, - RECENT_PAGES_LIST, -} from "constants/fetch-keys"; +import { PROJECT_DETAILS } from "constants/fetch-keys"; +import useUserAuth from "hooks/use-user-auth"; const AllPagesList = dynamic( () => import("components/pages").then((a) => a.AllPagesList), @@ -65,6 +57,8 @@ const OtherPagesList = dynamic( } ); +const tabsList = ["Recent", "All", "Favorites", "Created by me", "Created by others"]; + const ProjectPages: NextPage = () => { const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); @@ -73,22 +67,10 @@ const ProjectPages: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { setToastAlert } = useToast(); + const { user } = useUserAuth(); const { storedValue: pageTab, setValue: setPageTab } = useLocalStorage("pageTab", "Recent"); - const { - handleSubmit, - register, - watch, - reset, - formState: { isSubmitting }, - } = useForm>({ - defaultValues: { - name: "", - }, - }); - const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -96,58 +78,6 @@ const ProjectPages: NextPage = () => { : null ); - const createPage = async (formData: Partial) => { - if (!workspaceSlug || !projectId) return; - - if (formData.name === "") { - setToastAlert({ - type: "error", - title: "Error!", - message: "Page name is required", - }); - return; - } - - await pagesService - .createPage(workspaceSlug as string, projectId as string, formData) - .then((res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Page created successfully.", - }); - router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res.id}`); - reset(); - - mutate(RECENT_PAGES_LIST(projectId as string)); - mutate( - MY_PAGES_LIST(projectId as string), - (prevData) => { - if (!prevData) return undefined; - - return [res, ...(prevData as IPage[])]; - }, - false - ); - mutate( - ALL_PAGES_LIST(projectId as string), - (prevData) => { - if (!prevData) return undefined; - - return [res, ...(prevData as IPage[])]; - }, - false - ); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Page could not be created. Please try again", - }); - }); - }; - const currentTabValue = (tab: string | null) => { switch (tab) { case "Recent": @@ -171,11 +101,9 @@ const ProjectPages: NextPage = () => { setCreateUpdatePageModal(false)} + user={user} /> @@ -195,114 +123,85 @@ const ProjectPages: NextPage = () => { } > -
    +

    Pages

    - {/* - - {watch("name") !== "" && ( - - {isSubmitting ? "Creating..." : "Create"} - - )} - */} -
    - { - switch (i) { - case 0: - return setPageTab("Recent"); - case 1: - return setPageTab("All"); - case 2: - return setPageTab("Favorites"); - case 3: - return setPageTab("Created by me"); - case 4: - return setPageTab("Created by others"); + { + switch (i) { + case 0: + return setPageTab("Recent"); + case 1: + return setPageTab("All"); + case 2: + return setPageTab("Favorites"); + case 3: + return setPageTab("Created by me"); + case 4: + return setPageTab("Created by others"); - default: - return setPageTab("Recent"); - } - }} - > - -
    - {["Recent", "All", "Favorites", "Created by me", "Created by others"].map( - (tab, index) => ( - - `rounded-full border px-5 py-1.5 text-sm outline-none ${ - selected - ? "border-brand-accent bg-brand-accent text-white" - : "border-brand-base bg-brand-base hover:bg-brand-surface-1" - }` - } - > - {tab} - - ) - )} -
    -
    - - - {/* */} -
    -
    - - - - - - - - - - - - - - - - - -
    -
    + {tab} + + ))} +
    +
    + + +
    + + + + + + + + + + + + + + + + + + +
    diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/control.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/control.tsx index 932b9e323..c6e02d726 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/control.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/control.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from "react"; import { useRouter } from "next/router"; -import Image from "next/image"; import useSWR, { mutate } from "swr"; @@ -13,6 +12,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import projectService from "services/project.service"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // components import { SettingsHeader } from "components/project"; // ui @@ -35,6 +35,8 @@ const ControlSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -65,7 +67,7 @@ const ControlSettings: NextPage = () => { }; await projectService - .updateProject(workspaceSlug as string, projectId as string, payload) + .updateProject(workspaceSlug as string, projectId as string, payload, user) .then((res) => { mutate(PROJECT_DETAILS(projectId as string)); mutate(PROJECTS_LIST(workspaceSlug as string)); @@ -135,12 +137,10 @@ const ControlSettings: NextPage = () => {
    {person.member.avatar && person.member.avatar !== "" ? (
    - avatar
    ) : ( @@ -198,12 +198,10 @@ const ControlSettings: NextPage = () => {
    {person.member.avatar && person.member.avatar !== "" ? (
    - avatar
    ) : ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 3f3c0331f..76e353319 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -16,6 +16,7 @@ import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates" import { SettingsHeader } from "components/project"; //hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // ui import { EmptyState, Loader, SecondaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; @@ -37,6 +38,8 @@ const EstimatesSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { projectDetails } = useProjectDetails(); @@ -63,7 +66,7 @@ const EstimatesSettings: NextPage = () => { ); estimatesService - .deleteEstimate(workspaceSlug as string, projectId as string, estimateId) + .deleteEstimate(workspaceSlug as string, projectId as string, estimateId, user) .catch(() => { setToastAlert({ type: "error", @@ -87,7 +90,7 @@ const EstimatesSettings: NextPage = () => { ); projectService - .updateProject(workspaceSlug as string, projectId as string, { estimate: null }) + .updateProject(workspaceSlug as string, projectId as string, { estimate: null }, user) .catch(() => setToastAlert({ type: "error", @@ -106,6 +109,7 @@ const EstimatesSettings: NextPage = () => { setEstimateFormOpen(false); setEstimateToUpdate(undefined); }} + user={user} /> { estimate={estimate} editEstimate={(estimate) => editEstimate(estimate)} handleEstimateDelete={(estimateId) => removeEstimate(estimateId)} + user={user} /> ))} diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index 9f8d06b3a..6932ffbd7 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -11,6 +11,7 @@ import trackEventServices, { MiscellaneousEventType } from "services/track-event import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // components import { SettingsHeader } from "components/project"; // ui @@ -75,6 +76,8 @@ const FeaturesSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: projectDetails } = useSWR( @@ -134,7 +137,7 @@ const FeaturesSettings: NextPage = () => { }); await projectService - .updateProject(workspaceSlug as string, projectId as string, formData) + .updateProject(workspaceSlug as string, projectId as string, formData, user) .then(() => { mutate( projectDetails.is_favorite @@ -194,7 +197,8 @@ const FeaturesSettings: NextPage = () => { }, !projectDetails?.[feature.property as keyof IProject] ? getEventType(feature.title, true) - : getEventType(feature.title, false) + : getEventType(feature.title, false), + user ); handleSubmit({ [feature.property]: !projectDetails?.[feature.property as keyof IProject], diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 5210c9ebe..92b55cebe 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; @@ -17,6 +16,7 @@ import { ImagePickerPopover } from "components/core"; import EmojiIconPicker from "components/emoji-icon-picker"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // ui import { Input, @@ -45,6 +45,8 @@ const defaultValues: Partial = { const GeneralSettings: NextPage = () => { const [selectProject, setSelectedProject] = useState(null); + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const router = useRouter(); @@ -83,7 +85,7 @@ const GeneralSettings: NextPage = () => { if (!workspaceSlug || !projectDetails) return; await projectService - .updateProject(workspaceSlug as string, projectDetails.id, payload) + .updateProject(workspaceSlug as string, projectDetails.id, payload, user) .then((res) => { mutate( PROJECT_DETAILS(projectDetails.id), @@ -154,6 +156,7 @@ const GeneralSettings: NextPage = () => { onSuccess={() => { router.push(`/${workspaceSlug}/projects`); }} + user={user} />
    @@ -252,12 +255,10 @@ const GeneralSettings: NextPage = () => { {watch("cover_image") ? (
    - {projectDetails?.name
    { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const scrollToRef = useRef(null); const { data: projectDetails } = useSWR( @@ -85,11 +89,13 @@ const LabelsSettings: NextPage = () => { isOpen={labelsListModal} handleClose={() => setLabelsListModal(false)} parent={parentLabel} + user={user} /> setSelectDeleteLabel(null)} + user={user} /> { }); }} handleLabelDelete={() => setSelectDeleteLabel(label)} + user={user} /> ); }) diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index ce9110aad..92f490687 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -135,6 +134,7 @@ const MembersSettings: NextPage = () => { isOpen={inviteModal} setIsOpen={setInviteModal} members={members} + user={user} /> {
    {member.avatar && member.avatar !== "" ? ( - {member.first_name} ) : member.first_name !== "" ? ( member.first_name.charAt(0) diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index ac586f382..71a5046da 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -8,6 +8,7 @@ import useSWR from "swr"; import stateService from "services/state.service"; // hooks import useProjectDetails from "hooks/use-project-details"; +import useUserAuth from "hooks/use-user-auth"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // components @@ -38,6 +39,8 @@ const StatesSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { projectDetails } = useProjectDetails(); const { data: states } = useSWR( @@ -55,6 +58,7 @@ const StatesSettings: NextPage = () => { isOpen={!!selectDeleteState} data={statesList?.find((s) => s.id === selectDeleteState) ?? null} onClose={() => setSelectDeleteState(null)} + user={user} /> { }} data={null} selectedGroup={key as keyof StateGroup} + user={user} /> )} {orderedStateGroups[key].map((state, index) => @@ -111,6 +116,7 @@ const StatesSettings: NextPage = () => { statesList={statesList} handleEditState={() => setSelectedState(state.id)} handleDeleteState={() => setSelectDeleteState(state.id)} + user={user} /> ) : (
    { statesList?.find((state) => state.id === selectedState) ?? null } selectedGroup={key as keyof StateGroup} + user={user} />
    ) diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index c796525f6..44c25cdf0 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -4,6 +4,8 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // services import viewsService from "services/views.service"; import projectService from "services/project.service"; @@ -34,6 +36,8 @@ const ProjectViews: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { data: activeProject } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -60,9 +64,6 @@ const ProjectViews: NextPage = () => { return ( @@ -89,11 +90,13 @@ const ProjectViews: NextPage = () => { isOpen={createUpdateViewModal} handleClose={() => setCreateUpdateViewModal(false)} data={selectedViewToUpdate} + user={user} /> {views ? ( views.length > 0 ? ( diff --git a/apps/app/pages/[workspaceSlug]/projects/index.tsx b/apps/app/pages/[workspaceSlug]/projects/index.tsx index 6b29d41c5..fa34f02c0 100644 --- a/apps/app/pages/[workspaceSlug]/projects/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/index.tsx @@ -9,6 +9,7 @@ import projectService from "services/project.service"; // hooks import useProjects from "hooks/use-projects"; import useWorkspaces from "hooks/use-workspaces"; +import useUserAuth from "hooks/use-user-auth"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; // components @@ -30,6 +31,8 @@ const ProjectsPage: NextPage = () => { // router const router = useRouter(); const { workspaceSlug } = router.query; + + const { user } = useUserAuth(); // context data const { activeWorkspace } = useWorkspaces(); const { projects } = useProjects(); @@ -81,6 +84,7 @@ const ProjectsPage: NextPage = () => { isOpen={!!deleteProject} onClose={() => setDeleteProject(null)} data={projects?.find((item) => item.id === deleteProject) ?? null} + user={user} /> {projects ? (
    diff --git a/apps/app/pages/[workspaceSlug]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/settings/index.tsx index 8ebf74aa3..1ac94661b 100644 --- a/apps/app/pages/[workspaceSlug]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/index.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; @@ -12,6 +11,7 @@ import workspaceService from "services/workspace.service"; import fileService from "services/file.service"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import SettingsNavbar from "layouts/settings-navbar"; @@ -49,6 +49,8 @@ const WorkspaceSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: activeWorkspace } = useSWR( @@ -82,7 +84,7 @@ const WorkspaceSettings: NextPage = () => { }; await workspaceService - .updateWorkspace(activeWorkspace.slug, payload) + .updateWorkspace(activeWorkspace.slug, payload, user) .then((res) => { mutate(USER_WORKSPACES, (prevData) => prevData?.map((workspace) => (workspace.id === res.id ? res : workspace)) @@ -114,7 +116,7 @@ const WorkspaceSettings: NextPage = () => { fileService.deleteFile(asset).then(() => { workspaceService - .updateWorkspace(activeWorkspace.slug, { logo: "" }) + .updateWorkspace(activeWorkspace.slug, { logo: "" }, user) .then((res) => { setToastAlert({ type: "success", @@ -146,9 +148,6 @@ const WorkspaceSettings: NextPage = () => { return ( @@ -172,6 +171,7 @@ const WorkspaceSettings: NextPage = () => { setIsOpen(false); }} data={activeWorkspace ?? null} + user={user} />
    @@ -189,13 +189,10 @@ const WorkspaceSettings: NextPage = () => {