forked from github/plane
Merge pull request #1252 from makeplane/develop
promote: develop to stage release
This commit is contained in:
commit
d09f410f21
58
.env.example
58
.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 <team@mailer.plane.so>"
|
||||
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=""
|
||||
|
||||
# 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
|
23
Dockerfile
23
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"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
66
README.md
66
README.md
@ -15,11 +15,18 @@
|
||||
</a>
|
||||
<img alt="Discord" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<p>
|
||||
<a href="https://app.plane.so/" target="_blank">
|
||||
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
|
||||
<img
|
||||
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680599798/Plane/plane_1_1_tnb32j.png"
|
||||
src="https://ik.imagekit.io/killbluedog/Plane_Screen.png?updatedAt=1684942001069"
|
||||
alt="Plane Screens"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://app.plane.so/#gh-dark-mode-only" target="_blank">
|
||||
<img
|
||||
src="https://ik.imagekit.io/killbluedog/Plane_Screens_Dark_Mode.png?updatedAt=1684942388044"
|
||||
alt="Plane Screens"
|
||||
width="100%"
|
||||
/>
|
||||
@ -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
|
||||
```
|
||||
|
||||
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
|
||||
@ -89,41 +92,62 @@ docker-compose -f docker-compose-hub.yml up
|
||||
## 📸 Screenshots
|
||||
|
||||
<p>
|
||||
<a href="https://app.plane.so/" target="_blank">
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601719/Plane/plane_2_iqao52.png"
|
||||
src="https://ik.imagekit.io/killbluedog/Plane_Views_Dark_Mode.png?updatedAt=1684943050275"
|
||||
alt="Plane Views"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://ik.imagekit.io/killbluedog/Plane_Issue_Detail_Dark_Mode.png?updatedAt=1684943050202"
|
||||
alt="Plane Issue Details"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://app.plane.so/" target="_blank">
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680604273/Plane/plane_5_1_nwsl3a.png"
|
||||
src="https://ik.imagekit.io/killbluedog/Plane_Cycles___Modules_Dark_Mode.png?updatedAt=1684943050281"
|
||||
alt="Plane Cycles and Modules"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://app.plane.so/" target="_blank">
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601713/Plane/plane_4_cqm0g8.png"
|
||||
alt="Plane Quick Lists"
|
||||
src="https://ik.imagekit.io/killbluedog/Plane_Analytics_Dark_Mode.png?updatedAt=1684944596824"
|
||||
alt="Plane Analytics"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://app.plane.so/" target="_blank">
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601712/Plane/plane_3_1_cu4fsc.png"
|
||||
alt="Plane Command K"
|
||||
src="https://ik.imagekit.io/killbluedog/Plane_Pages_Dark_Mode.png?updatedAt=1684943050202"
|
||||
alt="Plane Pages"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://ik.imagekit.io/killbluedog/Plane_Commad_K_Dark_Mode.png?updatedAt=1684943050312"
|
||||
alt="Plane Command Menu"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
## 📚Documentation
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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/<uidb64>/<token>/",
|
||||
"reset-password/<uidb64>/<token>/",
|
||||
ResetPasswordEndpoint.as_view(),
|
||||
name="password-reset",
|
||||
),
|
||||
@ -664,21 +656,6 @@ urlpatterns = [
|
||||
CycleDateCheckEndpoint.as_view(),
|
||||
name="project-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/current-upcoming-cycles/",
|
||||
CurrentUpcomingCyclesEndpoint.as_view(),
|
||||
name="project-cycle-upcoming",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/completed-cycles/",
|
||||
CompletedCyclesEndpoint.as_view(),
|
||||
name="project-cycle-completed",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/draft-cycles/",
|
||||
DraftCyclesEndpoint.as_view(),
|
||||
name="project-cycle-draft",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
|
||||
CycleFavoriteViewSet.as_view(
|
||||
@ -703,11 +680,6 @@ urlpatterns = [
|
||||
TransferCycleIssueEndpoint.as_view(),
|
||||
name="transfer-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/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/<str:slug>/projects/<uuid:project_id>/pages/recent-pages/",
|
||||
RecentPagesEndpoint.as_view(),
|
||||
name="recent-pages",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/favorite-pages/",
|
||||
FavoritePagesEndpoint.as_view(),
|
||||
name="recent-pages",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/my-pages/",
|
||||
MyPagesEndpoint.as_view(),
|
||||
name="user-pages",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/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"),
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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"
|
||||
|
@ -126,6 +126,56 @@ class PageViewSet(BaseViewSet):
|
||||
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,
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.")
|
||||
|
||||
|
||||
|
@ -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.")
|
||||
|
||||
|
||||
|
@ -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)
|
@ -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)
|
||||
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -1,11 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<body>
|
||||
<p>
|
||||
Dear {{first_name}},<br /><br />
|
||||
Welcome! Your account has been created.
|
||||
Verify your email by clicking on the link below <br />
|
||||
{{forgot_password_url}}
|
||||
successfully.<br /><br />
|
||||
We received a request to reset your password for your Plane account.
|
||||
<br /><br />
|
||||
To proceed with resetting your password, please click on the link below:
|
||||
<br />
|
||||
<a href="{{forgot_password_url}}">{{forgot_password_url}}</a>
|
||||
<br /><br />
|
||||
If you didn't request to reset your password, please ignore this email. Your account will remain secure.
|
||||
<br /><br />
|
||||
If you have any questions or need further assistance, please contact our support team.
|
||||
<br /><br />
|
||||
Thank you for using Plane.
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
26
app.json
26
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": ""
|
||||
|
@ -1,4 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
rules: {
|
||||
"@next/next/no-img-element": "off",
|
||||
},
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
FROM node:18-alpine
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,12 +65,9 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
};
|
||||
|
||||
const handleSignin = async (formData: EmailCodeFormValues) => {
|
||||
await authenticationService
|
||||
.magicSignIn(formData)
|
||||
.then((response) => {
|
||||
onSuccess(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(true);
|
||||
await authenticationService.magicSignIn(formData).catch((error) => {
|
||||
setIsLoading(false);
|
||||
setToastAlert({
|
||||
title: "Oops!",
|
||||
type: "error",
|
||||
@ -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 (
|
||||
<>
|
||||
<form className="space-y-5 py-5 px-5">
|
||||
@ -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"}
|
||||
</PrimaryButton>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
|
@ -1,6 +1,4 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import React, { useState } from "react";
|
||||
|
||||
// react hook form
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -8,6 +6,8 @@ import { useForm } from "react-hook-form";
|
||||
import authenticationService from "services/authentication.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmailResetPasswordForm } from "components/account";
|
||||
// ui
|
||||
import { Input, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
@ -17,8 +17,11 @@ type EmailPasswordFormValues = {
|
||||
medium?: string;
|
||||
};
|
||||
|
||||
export const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
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,8 +61,12 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isResettingPassword ? (
|
||||
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
|
||||
) : (
|
||||
<form className="mt-5 py-5 px-5" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<Input
|
||||
@ -93,11 +100,13 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="ml-auto text-sm">
|
||||
<Link href={"/forgot-password"}>
|
||||
<a className="font-medium text-brand-accent hover:text-brand-accent">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsResettingPassword(true)}
|
||||
className="font-medium text-brand-accent hover:text-brand-accent"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
@ -111,6 +120,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
93
apps/app/components/account/email-reset-password-form.tsx
Normal file
93
apps/app/components/account/email-reset-password-form.tsx
Normal file
@ -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<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const EmailResetPasswordForm: React.FC<Props> = ({ 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 (
|
||||
<form className="mt-5 py-5 px-5" onSubmit={handleSubmit(forgotPassword)}>
|
||||
<div>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Email ID is required",
|
||||
validate: (value) =>
|
||||
/^(([^<>()[\]\\.,;:\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"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center gap-2">
|
||||
<SecondaryButton
|
||||
className="w-full text-center"
|
||||
onClick={() => setIsResettingPassword(false)}
|
||||
>
|
||||
Go Back
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type="submit" className="w-full text-center" loading={isSubmitting}>
|
||||
{isSubmitting ? "Sending link..." : "Send reset link"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -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<EmailSignInFormProps> = (props) => {
|
||||
const { handleSuccess } = props;
|
||||
// states
|
||||
const [useCode, setUseCode] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{useCode ? (
|
||||
<EmailCodeForm onSuccess={handleSuccess} />
|
||||
) : (
|
||||
<EmailPasswordForm onSuccess={handleSuccess} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -29,7 +29,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
useEffect(() => {
|
||||
const origin =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
setLoginCallBackURL(`${origin}/signin` as any);
|
||||
setLoginCallBackURL(`${origin}/` as any);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -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";
|
||||
|
@ -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<IAnalyticsParams, any>;
|
||||
setValue: UseFormSetValue<IAnalyticsParams>;
|
||||
fullScreen: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const CustomAnalytics: React.FC<Props> = ({
|
||||
@ -38,6 +39,7 @@ export const CustomAnalytics: React.FC<Props> = ({
|
||||
control,
|
||||
setValue,
|
||||
fullScreen,
|
||||
user,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -124,6 +126,7 @@ export const CustomAnalytics: React.FC<Props> = ({
|
||||
params={params}
|
||||
fullScreen={fullScreen}
|
||||
isProjectLevel={isProjectLevel}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -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<Props> = ({
|
||||
@ -41,6 +50,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
params,
|
||||
fullScreen,
|
||||
isProjectLevel = false,
|
||||
user,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
@ -82,6 +92,60 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
: 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<Props> = ({
|
||||
|
||||
analyticsService
|
||||
.exportAnalytics(workspaceSlug.toString(), data)
|
||||
.then((res) =>
|
||||
.then((res) => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: res.message,
|
||||
});
|
||||
|
||||
trackExportAnalytics();
|
||||
})
|
||||
)
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
|
@ -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<Props> = ({ isOpen, onClose }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { control, watch, setValue } = useForm<IAnalyticsParams>({ defaultValues });
|
||||
|
||||
const params: IAnalyticsParams = {
|
||||
@ -95,6 +99,51 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ 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<Props> = ({ isOpen, onClose }) => {
|
||||
selected ? "bg-brand-surface-2" : ""
|
||||
}`
|
||||
}
|
||||
onClick={() => trackAnalyticsEvent(tab)}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
@ -164,6 +214,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
fullScreen={fullScreen}
|
||||
user={user}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
|
@ -1,5 +1,3 @@
|
||||
import Image from "next/image";
|
||||
|
||||
type Props = {
|
||||
users: {
|
||||
avatar: string | null;
|
||||
@ -23,12 +21,10 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{user && user.avatar && user.avatar !== "" ? (
|
||||
<div className="rounded-full h-4 w-4 flex-shrink-0">
|
||||
<Image
|
||||
<div className="relative rounded-full h-4 w-4 flex-shrink-0">
|
||||
<img
|
||||
src={user.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
|
||||
alt={user.email ?? "None"}
|
||||
/>
|
||||
</div>
|
||||
|
@ -21,12 +21,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
||||
const { asPath: currentPath } = useRouter();
|
||||
|
||||
return (
|
||||
<DefaultLayout
|
||||
meta={{
|
||||
title: "Plane - Not Authorized",
|
||||
description: "You are not authorized to view this page",
|
||||
}}
|
||||
>
|
||||
<DefaultLayout>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-brand-surface-1 text-center">
|
||||
<div className="h-44 w-72">
|
||||
<Image
|
||||
@ -44,7 +39,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
||||
{user ? (
|
||||
<p>
|
||||
You have signed in as {user.email}. <br />
|
||||
<Link href={`/signin?next=${currentPath}`}>
|
||||
<Link href={`/?next=${currentPath}`}>
|
||||
<a className="font-medium text-brand-base">Sign in</a>
|
||||
</Link>{" "}
|
||||
with different account that has access to this page.
|
||||
@ -52,7 +47,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
||||
) : (
|
||||
<p>
|
||||
You need to{" "}
|
||||
<Link href={`/signin?next=${currentPath}`}>
|
||||
<Link href={`/?next=${currentPath}`}>
|
||||
<a className="font-medium text-brand-base">Sign in</a>
|
||||
</Link>{" "}
|
||||
with an account that has access to this page.
|
||||
|
@ -1,28 +1,19 @@
|
||||
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 (
|
||||
<DefaultLayout
|
||||
meta={{
|
||||
title: "Plane - Unauthorized User",
|
||||
description: "Unauthorized user",
|
||||
}}
|
||||
>
|
||||
export const NotAWorkspaceMember = () => (
|
||||
<DefaultLayout>
|
||||
<div className="grid h-full place-items-center p-4">
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Not Authorized!</h3>
|
||||
<p className="mx-auto w-1/2 text-sm text-brand-secondary">
|
||||
You{"'"}re not a member of this workspace. Please contact the workspace admin to get
|
||||
an invitation or check your pending invitations.
|
||||
You{"'"}re not a member of this workspace. Please contact the workspace admin to get an
|
||||
invitation or check your pending invitations.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
@ -41,4 +32,3 @@ export const NotAWorkspaceMember = () => {
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
);
|
||||
};
|
||||
|
@ -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) => {
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
|
||||
className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeftIcon className="h-3 w-3" />
|
||||
<Icon
|
||||
iconName="keyboard_backspace"
|
||||
className="text-base leading-4 text-brand-secondary group-hover:text-brand-base"
|
||||
/>
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -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<SetStateAction<boolean>>;
|
||||
issue: IIssue;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue }) => {
|
||||
export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
@ -57,18 +58,21 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate(
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prevData: IIssue) => ({
|
||||
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<Props> = ({ 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);
|
||||
|
@ -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<SetStateAction<boolean>>;
|
||||
issue: IIssue;
|
||||
user: ICurrentUserResponse;
|
||||
};
|
||||
|
||||
export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue }) => {
|
||||
export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
@ -27,18 +28,22 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate(
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prevData: IIssue) => ({
|
||||
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));
|
||||
})
|
||||
|
@ -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<SetStateAction<boolean>>;
|
||||
issue: IIssue;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) => {
|
||||
export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
@ -39,18 +40,21 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate(
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prevData: IIssue) => ({
|
||||
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));
|
||||
|
@ -120,18 +120,23 @@ export const CommandPalette: React.FC = () => {
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate(
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prevData: IIssue) => ({
|
||||
|
||||
(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 = () => {
|
||||
<>
|
||||
<ShortcutsModal isOpen={isShortcutsModalOpen} setIsOpen={setIsShortcutsModalOpen} />
|
||||
{workspaceSlug && (
|
||||
<CreateProjectModal isOpen={isProjectModalOpen} setIsOpen={setIsProjectModalOpen} />
|
||||
<CreateProjectModal
|
||||
isOpen={isProjectModalOpen}
|
||||
setIsOpen={setIsProjectModalOpen}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
{projectId && (
|
||||
<>
|
||||
<CreateUpdateCycleModal
|
||||
isOpen={isCreateCycleModalOpen}
|
||||
handleClose={() => setIsCreateCycleModalOpen(false)}
|
||||
user={user}
|
||||
/>
|
||||
<CreateUpdateModuleModal
|
||||
isOpen={isCreateModuleModalOpen}
|
||||
setIsOpen={setIsCreateModuleModalOpen}
|
||||
user={user}
|
||||
/>
|
||||
<CreateUpdateViewModal
|
||||
handleClose={() => setIsCreateViewModalOpen(false)}
|
||||
isOpen={isCreateViewModalOpen}
|
||||
user={user}
|
||||
/>
|
||||
<CreateUpdatePageModal
|
||||
isOpen={isCreateUpdatePageModalOpen}
|
||||
handleClose={() => 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 = () => {
|
||||
<BulkDeleteIssuesModal
|
||||
isOpen={isBulkDeleteIssuesModalOpen}
|
||||
setIsOpen={setIsBulkDeleteIssuesModalOpen}
|
||||
user={user}
|
||||
/>
|
||||
<Transition.Root
|
||||
show={isPaletteOpen}
|
||||
@ -849,6 +864,7 @@ export const CommandPalette: React.FC = () => {
|
||||
<ChangeIssueState
|
||||
issue={issueDetails}
|
||||
setIsPaletteOpen={setIsPaletteOpen}
|
||||
user={user}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -856,12 +872,14 @@ export const CommandPalette: React.FC = () => {
|
||||
<ChangeIssuePriority
|
||||
issue={issueDetails}
|
||||
setIsPaletteOpen={setIsPaletteOpen}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
{page === "change-issue-assignee" && issueDetails && (
|
||||
<ChangeIssueAssignee
|
||||
issue={issueDetails}
|
||||
setIsPaletteOpen={setIsPaletteOpen}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
{page === "change-interface-theme" && (
|
||||
|
@ -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<Props> = ({
|
||||
handleTrashBox,
|
||||
removeIssue,
|
||||
isCompleted = false,
|
||||
user,
|
||||
userAuth,
|
||||
}) => {
|
||||
const {
|
||||
@ -65,6 +67,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
handleTrashBox={handleTrashBox}
|
||||
removeIssue={removeIssue}
|
||||
isCompleted={isCompleted}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
);
|
||||
|
@ -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<Props> = ({
|
||||
handleTrashBox,
|
||||
removeIssue,
|
||||
isCompleted = false,
|
||||
user,
|
||||
userAuth,
|
||||
}) => {
|
||||
// collapse/expand
|
||||
@ -129,6 +131,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
removeIssue(issue.bridge_id, issue.id);
|
||||
}}
|
||||
isCompleted={isCompleted}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
)}
|
||||
|
@ -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<Props> = ({
|
||||
handleDeleteIssue,
|
||||
handleTrashBox,
|
||||
isCompleted = false,
|
||||
user,
|
||||
userAuth,
|
||||
}) => {
|
||||
// context menu
|
||||
@ -170,7 +172,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
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<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
/>
|
||||
)}
|
||||
@ -350,6 +353,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
/>
|
||||
)}
|
||||
@ -357,6 +361,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
@ -384,6 +389,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
tooltipPosition="left"
|
||||
user={user}
|
||||
selfPositioned
|
||||
/>
|
||||
)}
|
||||
@ -392,6 +398,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
/>
|
||||
)}
|
||||
|
@ -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<React.SetStateAction<boolean>>;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
@ -91,9 +92,14 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
||||
|
||||
if (workspaceSlug && projectId) {
|
||||
await issuesServices
|
||||
.bulkDeleteIssues(workspaceSlug as string, projectId as string, {
|
||||
.bulkDeleteIssues(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
{
|
||||
issue_ids: data.delete_issue_ids,
|
||||
})
|
||||
},
|
||||
user
|
||||
)
|
||||
.then((res) => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
|
@ -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<Props> = ({
|
||||
handleDeleteIssue,
|
||||
addIssueToDate,
|
||||
isCompleted = false,
|
||||
user,
|
||||
userAuth,
|
||||
}) => {
|
||||
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
||||
@ -134,9 +136,15 @@ export const CalendarView: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, draggableId, {
|
||||
.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<Props> = ({
|
||||
addIssueToDate={addIssueToDate}
|
||||
isMonthlyView={isMonthlyView}
|
||||
showWeekEnds={showWeekEnds}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
|
@ -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<Props> = ({
|
||||
addIssueToDate,
|
||||
isMonthlyView,
|
||||
showWeekEnds,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
@ -72,6 +74,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
issue={issue}
|
||||
handleEditIssue={handleEditIssue}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
|
@ -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<Props> = ({
|
||||
provided,
|
||||
snapshot,
|
||||
issue,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -95,7 +97,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
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<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
@ -192,6 +195,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -199,6 +203,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
@ -227,6 +232,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
@ -235,6 +241,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
|
@ -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<any> = ({ activities }) => (
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<Image
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={30}
|
||||
@ -276,7 +276,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||
) : activity.actor_detail.avatar &&
|
||||
activity.actor_detail.avatar !== "" ? (
|
||||
<Image
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={24}
|
||||
|
@ -10,6 +10,7 @@ import aiService from "services/ai.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// ui
|
||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
|
||||
@ -60,6 +61,8 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@ -97,10 +100,15 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
await aiService
|
||||
.createGptTask(workspaceSlug as string, projectId as string, {
|
||||
.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<Props> = ({
|
||||
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
|
||||
|
@ -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<Props> = ({ label, value, onChange })
|
||||
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
||||
placeholder="Search for images"
|
||||
/>
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
onClick={() => setSearchParams(formData.search)}
|
||||
className="bg-indigo-600"
|
||||
size="sm"
|
||||
>
|
||||
<PrimaryButton onClick={() => setSearchParams(formData.search)} size="sm">
|
||||
Search
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
@ -123,12 +115,10 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
||||
key={image.id}
|
||||
className="relative col-span-2 aspect-video md:col-span-1"
|
||||
>
|
||||
<Image
|
||||
<img
|
||||
src={image.urls.small}
|
||||
alt={image.alt_description}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="cursor-pointer rounded"
|
||||
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
onChange(image.urls.regular);
|
||||
|
@ -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<Props> = ({
|
||||
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
@ -220,11 +223,17 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
// patch request
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||
.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<Props> = ({
|
||||
sourceStateBeforeDrag?.group !== "completed" &&
|
||||
response?.state_detail?.group === "completed"
|
||||
)
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent({
|
||||
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<Props> = ({
|
||||
isOpen={createViewModal !== null}
|
||||
handleClose={() => setCreateViewModal(null)}
|
||||
preLoadedData={createViewModal}
|
||||
user={user}
|
||||
/>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||
@ -437,6 +450,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
isOpen={deleteIssueModal}
|
||||
data={issueToDelete}
|
||||
user={user}
|
||||
/>
|
||||
<TransferIssuesModal
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
@ -508,6 +522,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
: null
|
||||
}
|
||||
isCompleted={isCompleted}
|
||||
user={user}
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : issueView === "kanban" ? (
|
||||
@ -528,6 +543,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
: null
|
||||
}
|
||||
isCompleted={isCompleted}
|
||||
user={user}
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : issueView === "calendar" ? (
|
||||
@ -536,6 +552,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
addIssueToDate={addIssueToDate}
|
||||
isCompleted={isCompleted}
|
||||
user={user}
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : (
|
||||
|
@ -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<Props> = ({
|
||||
handleDeleteIssue,
|
||||
removeIssue,
|
||||
isCompleted = false,
|
||||
user,
|
||||
userAuth,
|
||||
}) => {
|
||||
const { groupedByIssues, groupByProperty: selectedGroup, showEmptyGroups } = useIssuesView();
|
||||
@ -58,6 +60,7 @@ export const AllLists: React.FC<Props> = ({
|
||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
||||
removeIssue={removeIssue}
|
||||
isCompleted={isCompleted}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
);
|
||||
|
@ -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<Props> = ({
|
||||
groupTitle,
|
||||
handleDeleteIssue,
|
||||
isCompleted = false,
|
||||
user,
|
||||
userAuth,
|
||||
}) => {
|
||||
// context menu
|
||||
@ -141,7 +143,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
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<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
@ -249,6 +252,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
@ -256,6 +260,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
@ -284,6 +289,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
@ -292,6 +298,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
|
@ -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<Props> = ({
|
||||
openIssuesListModal,
|
||||
removeIssue,
|
||||
isCompleted = false,
|
||||
user,
|
||||
userAuth,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -208,6 +217,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
removeIssue(issue.bridge_id, issue.id);
|
||||
}}
|
||||
isCompleted={isCompleted}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))
|
||||
|
@ -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<TSingleStatProps> = ({ cycle, isCompleted = false }) => {
|
||||
export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -111,51 +97,18 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isComple
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
switch (cycleStatus) {
|
||||
case "current":
|
||||
case "upcoming":
|
||||
mutate<CurrentAndUpcomingCyclesResponse>(
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
current_cycle: (prevData?.current_cycle ?? []).map((c) => ({
|
||||
mutate<ICycle[]>(
|
||||
CURRENT_CYCLE_LIST(projectId as string),
|
||||
(prevData) =>
|
||||
(prevData ?? []).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<CompletedCyclesResponse>(
|
||||
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<DraftCyclesResponse>(
|
||||
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),
|
||||
CYCLES_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
@ -180,51 +133,18 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isComple
|
||||
const handleRemoveFromFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
switch (cycleStatus) {
|
||||
case "current":
|
||||
case "upcoming":
|
||||
mutate<CurrentAndUpcomingCyclesResponse>(
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
current_cycle: (prevData?.current_cycle ?? []).map((c) => ({
|
||||
mutate<ICycle[]>(
|
||||
CURRENT_CYCLE_LIST(projectId as string),
|
||||
(prevData) =>
|
||||
(prevData ?? []).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<CompletedCyclesResponse>(
|
||||
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<DraftCyclesResponse>(
|
||||
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),
|
||||
CYCLES_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
@ -244,17 +164,20 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isComple
|
||||
});
|
||||
};
|
||||
|
||||
const { data: issues } = useSWR<IIssue[]>(
|
||||
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<TSingleStatProps> = ({ cycle, isComple
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2.5 text-brand-secondary">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<Image
|
||||
<img
|
||||
src={cycle.owned_by.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@ -16,8 +15,6 @@ import useLocalStorage from "hooks/use-local-storage";
|
||||
import { SingleProgressStats } from "components/core";
|
||||
// ui
|
||||
import { Avatar } from "components/ui";
|
||||
// icons
|
||||
import User from "public/user.png";
|
||||
// types
|
||||
import { IIssue, IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
@ -125,9 +122,9 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ issues }) => {
|
||||
<SingleProgressStats
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-brand-surface-2">
|
||||
<Image
|
||||
src={User}
|
||||
<div className="h-5 w-5 rounded-full border-2 border-brand-base bg-brand-surface-2">
|
||||
<img
|
||||
src="/user.png"
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
|
@ -1,82 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// components
|
||||
import { DeleteCycleModal, SingleCycleCard } 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<React.SetStateAction<boolean>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
type: "current" | "upcoming" | "draft";
|
||||
};
|
||||
|
||||
export const AllCyclesBoard: React.FC<TCycleStatsViewProps> = ({
|
||||
cycles,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
type,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
||||
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
|
||||
setCycleDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycle({ ...cycle, actionType: "edit" });
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteCycleModal
|
||||
isOpen={
|
||||
cycleDeleteModal &&
|
||||
!!selectedCycleForDelete &&
|
||||
selectedCycleForDelete.actionType === "delete"
|
||||
}
|
||||
setIsOpen={setCycleDeleteModal}
|
||||
data={selectedCycleForDelete}
|
||||
/>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : type === "current" ? (
|
||||
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
|
||||
<h3 className="text-base font-medium text-brand-base ">No cycle is present.</h3>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project
|
||||
to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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<React.SetStateAction<boolean>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
type: "current" | "upcoming" | "draft";
|
||||
};
|
||||
|
||||
export const AllCyclesList: React.FC<TCycleStatsViewProps> = ({
|
||||
cycles,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
type,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
||||
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
|
||||
setCycleDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycle({ ...cycle, actionType: "edit" });
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteCycleModal
|
||||
isOpen={
|
||||
cycleDeleteModal &&
|
||||
!!selectedCycleForDelete &&
|
||||
selectedCycleForDelete.actionType === "delete"
|
||||
}
|
||||
setIsOpen={setCycleDeleteModal}
|
||||
data={selectedCycleForDelete}
|
||||
/>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
<div>
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col border-brand-base">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : type === "current" ? (
|
||||
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
|
||||
<h3 className="text-base font-medium text-brand-base ">No cycle is present.</h3>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project
|
||||
to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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<React.SetStateAction<boolean>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
}
|
||||
|
||||
export const CompletedCycles: React.FC<CompletedCyclesListProps> = ({
|
||||
cycleView,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<DeleteCycleModal
|
||||
isOpen={
|
||||
cycleDeleteModal &&
|
||||
!!selectedCycleForDelete &&
|
||||
selectedCycleForDelete.actionType === "delete"
|
||||
}
|
||||
setIsOpen={setCycleDeleteModal}
|
||||
data={selectedCycleForDelete}
|
||||
/>
|
||||
{completedCycles ? (
|
||||
completedCycles.completed_cycles.length > 0 ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-brand-secondary">
|
||||
<ExclamationIcon
|
||||
height={14}
|
||||
width={14}
|
||||
className="fill-current text-brand-secondary"
|
||||
/>
|
||||
<span>Completed cycles are not editable.</span>
|
||||
</div>
|
||||
{cycleView === "list" && (
|
||||
<div>
|
||||
{completedCycles.completed_cycles.map((cycle) => (
|
||||
<div className="hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col border-brand-base">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
isCompleted
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{cycleView === "board" && (
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{completedCycles.completed_cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
isCompleted
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project
|
||||
to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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<Props> = ({ cycles }) => {
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${data?.id}`}>
|
||||
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
|
||||
<div className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: "#858e96" }} />
|
||||
<div className="w-full text-brand-base text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden">
|
||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
||||
<div className="text-brand-base text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
||||
{data?.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
|
29
apps/app/components/cycles/cycles-list/all-cycles-list.tsx
Normal file
29
apps/app/components/cycles/cycles-list/all-cycles-list.tsx
Normal file
@ -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<Props> = ({ 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 <CyclesView cycles={allCyclesList} viewType={viewType} />;
|
||||
};
|
@ -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<Props> = ({ 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 <CyclesView cycles={completedCyclesList} viewType={viewType} />;
|
||||
};
|
29
apps/app/components/cycles/cycles-list/draft-cycles-list.tsx
Normal file
29
apps/app/components/cycles/cycles-list/draft-cycles-list.tsx
Normal file
@ -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<Props> = ({ 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 <CyclesView cycles={draftCyclesList} viewType={viewType} />;
|
||||
};
|
4
apps/app/components/cycles/cycles-list/index.ts
Normal file
4
apps/app/components/cycles/cycles-list/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./all-cycles-list";
|
||||
export * from "./completed-cycles-list";
|
||||
export * from "./draft-cycles-list";
|
||||
export * from "./upcoming-cycles-list";
|
@ -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<Props> = ({ 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 <CyclesView cycles={upcomingCyclesList} viewType={viewType} />;
|
||||
};
|
@ -1,189 +1,208 @@
|
||||
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<React.SetStateAction<SelectCycleType>>;
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
cyclesCompleteList: ICycle[] | undefined;
|
||||
currentAndUpcomingCycles: CurrentAndUpcomingCyclesResponse | undefined;
|
||||
draftCycles: DraftCyclesResponse | undefined;
|
||||
cycles: ICycle[] | undefined;
|
||||
viewType: string | null;
|
||||
};
|
||||
|
||||
export const CyclesView: React.FC<Props> = ({
|
||||
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<Props> = ({ cycles, viewType }) => {
|
||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||
const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState<ICycle | null>(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<ICycle | null>(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<CompletedCyclesListProps>(
|
||||
() => import("components/cycles").then((a) => a.CompletedCycles),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<Loader className="mb-5">
|
||||
<Loader.Item height="12rem" width="100%" />
|
||||
</Loader>
|
||||
),
|
||||
}
|
||||
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<ICycle[]>(
|
||||
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<ICycle[]>(
|
||||
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 (
|
||||
<>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<h3 className="text-2xl font-semibold text-brand-base">Cycles</h3>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
cyclesView === "list" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => setCyclesView("list")}
|
||||
>
|
||||
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
cyclesView === "board" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => setCyclesView("board")}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
cyclesView === "gantt_chart" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCyclesView("gantt_chart");
|
||||
setCycleTab("All");
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-rounded text-brand-secondary text-[18px] rotate-90">
|
||||
waterfall_chart
|
||||
</span>
|
||||
</button>
|
||||
<CreateUpdateCycleModal
|
||||
isOpen={createUpdateCycleModal}
|
||||
handleClose={() => setCreateUpdateCycleModal(false)}
|
||||
data={selectedCycleToUpdate}
|
||||
user={user}
|
||||
/>
|
||||
<DeleteCycleModal
|
||||
isOpen={deleteCycleModal}
|
||||
setIsOpen={setDeleteCycleModal}
|
||||
data={selectedCycleToDelete}
|
||||
user={user}
|
||||
/>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
viewType === "list" ? (
|
||||
<div className="divide-y divide-brand-base">
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col border-brand-base">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(cycle)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Tab.Group
|
||||
as={React.Fragment}
|
||||
defaultIndex={currentTabValue(cycleTab)}
|
||||
selectedIndex={currentTabValue(cycleTab)}
|
||||
onChange={(i) => {
|
||||
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");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<Tab.List as="div" className="flex flex-wrap items-center justify-start gap-4 text-base">
|
||||
{["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => {
|
||||
if (
|
||||
cyclesView === "gantt_chart" &&
|
||||
(tab === "Active" || tab === "Drafts" || tab === "Completed")
|
||||
))}
|
||||
</div>
|
||||
) : viewType === "board" ? (
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(cycle)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<CyclesListGanttChartView cycles={cycles ?? []} />
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={index}
|
||||
className={({ selected }) =>
|
||||
`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}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</Tab.List>
|
||||
</div>
|
||||
<Tab.Panels as={React.Fragment}>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
|
||||
{cyclesView === "list" && (
|
||||
<AllCyclesList
|
||||
cycles={cyclesCompleteList}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="current"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "board" && (
|
||||
<AllCyclesBoard
|
||||
cycles={cyclesCompleteList}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="current"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "gantt_chart" && (
|
||||
<CyclesListGanttChartView cycles={cyclesCompleteList ?? []} />
|
||||
)}
|
||||
</Tab.Panel>
|
||||
{cyclesView !== "gantt_chart" && (
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{currentAndUpcomingCycles?.current_cycle?.[0] ? (
|
||||
<ActiveCycleDetails cycle={currentAndUpcomingCycles?.current_cycle?.[0]} />
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
@ -191,59 +210,24 @@ export const CyclesView: React.FC<Props> = ({
|
||||
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)
|
||||
) : viewType === "list" ? (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
) : viewType === "board" ? (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="300px" />
|
||||
</Loader>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
)}
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
|
||||
{cyclesView === "list" && (
|
||||
<AllCyclesList
|
||||
cycles={currentAndUpcomingCycles?.upcoming_cycle}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="upcoming"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "board" && (
|
||||
<AllCyclesBoard
|
||||
cycles={currentAndUpcomingCycles?.upcoming_cycle}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="upcoming"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "gantt_chart" && (
|
||||
<CyclesListGanttChartView cycles={currentAndUpcomingCycles?.upcoming_cycle ?? []} />
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
<CompletedCycles
|
||||
cycleView={cyclesView ?? "list"}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
{cyclesView !== "gantt_chart" && (
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{cyclesView === "list" && (
|
||||
<AllCyclesList
|
||||
cycles={draftCycles?.draft_cycles}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="draft"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "board" && (
|
||||
<AllCyclesBoard
|
||||
cycles={draftCycles?.draft_cycles}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="draft"
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
)}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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<React.SetStateAction<boolean>>;
|
||||
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<TConfirmCycleDeletionProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
data,
|
||||
user,
|
||||
}) => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
@ -58,65 +55,30 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
|
||||
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<CompletedCyclesResponse>(
|
||||
CYCLE_COMPLETE_LIST(projectId as string),
|
||||
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);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) => {
|
||||
if (!prevData) return;
|
||||
|
||||
return {
|
||||
completed_cycles: prevData.completed_cycles?.filter(
|
||||
(cycle) => cycle.id !== data?.id
|
||||
),
|
||||
};
|
||||
return prevData.filter((cycle) => cycle.id !== data?.id);
|
||||
},
|
||||
false
|
||||
);
|
||||
break;
|
||||
case "current":
|
||||
mutate<CurrentAndUpcomingCyclesResponse>(
|
||||
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<CurrentAndUpcomingCyclesResponse>(
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return;
|
||||
|
||||
return {
|
||||
current_cycle: prevData.current_cycle,
|
||||
upcoming_cycle: prevData.upcoming_cycle?.filter((c) => c.id !== data?.id),
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
break;
|
||||
default:
|
||||
mutate<DraftCyclesResponse>(
|
||||
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);
|
||||
|
@ -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 (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-5 ">
|
||||
<div className="relative h-32 w-72">
|
||||
<div className="absolute right-0 top-0 flex w-64 flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
|
||||
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
|
||||
<span className="text-sm font-semibold text-brand-base">Cycle Name</span>
|
||||
<div className="flex h-full w-full items-center gap-4">
|
||||
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-brand-base bg-brand-surface-1 px-4 py-3">
|
||||
<LinearProgressIndicator data={emptyCycleData} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-0 bottom-0 flex w-64 flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
|
||||
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
|
||||
<span className="text-sm font-semibold text-brand-base">Cycle Name</span>
|
||||
<div className="flex h-full w-full items-center gap-4">
|
||||
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-brand-base bg-brand-surface-1 px-4 py-3">
|
||||
<LinearProgressIndicator data={emptyCycleData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-4 text-center ">
|
||||
<h3 className="text-xl font-semibold">Create New Cycle</h3>
|
||||
<p className="text-sm text-brand-secondary">
|
||||
Sprint more effectively with Cycles by confining your project <br /> to a fixed amount of
|
||||
time. Create new cycle now.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -12,7 +12,7 @@ type Props = {
|
||||
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
|
||||
handleClose: () => void;
|
||||
status: boolean;
|
||||
data?: ICycle;
|
||||
data?: ICycle | null;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<ICycle> = {
|
||||
@ -28,7 +28,6 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
reset,
|
||||
} = useForm<ICycle>({
|
||||
defaultValues,
|
||||
|
@ -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<Props> = ({}) => {
|
||||
className="flex-shrink-0 w-[4px] h-full"
|
||||
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
|
||||
/>
|
||||
<div className="w-full text-brand-base text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden">
|
||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
||||
<div className="text-brand-base text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
||||
{data?.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{data.infoToggle && (
|
||||
<Tooltip
|
||||
tooltipContent={`No due-date set, rendered according to last updated date.`}
|
||||
className={`z-[999999]`}
|
||||
>
|
||||
<div className="flex-shrink-0 mx-2 w-[18px] h-[18px] overflow-hidden flex justify-center items-center">
|
||||
<span className="material-symbols-rounded text-brand-secondary text-[18px]">
|
||||
info
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
@ -59,10 +75,20 @@ export const CycleIssuesGanttChartView: FC<Props> = ({}) => {
|
||||
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,
|
||||
};
|
||||
})
|
||||
|
@ -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";
|
||||
|
@ -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<CycleModalProps> = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
data,
|
||||
user,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -42,24 +45,26 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const createCycle = async (payload: Partial<ICycle>) => {
|
||||
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<CycleModalProps> = ({
|
||||
message: "Cycle created successfully.",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
@ -78,39 +83,41 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
};
|
||||
|
||||
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<IssueCycleSelectProps> = ({
|
||||
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<IssueCycleSelectProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateCycleModal isOpen={isCycleModalActive} handleClose={closeCycleModal} />
|
||||
<CreateUpdateCycleModal
|
||||
isOpen={isCycleModalActive}
|
||||
handleClose={closeCycleModal}
|
||||
user={user}
|
||||
/>
|
||||
<Listbox as="div" className="relative" value={value} onChange={onChange} multiple={multiple}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
|
@ -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<Props> = ({
|
||||
@ -55,6 +55,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
isOpen,
|
||||
cycleStatus,
|
||||
isCompleted,
|
||||
user,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
|
||||
@ -94,7 +95,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
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<Props> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteCycleModal isOpen={cycleDeleteModal} setIsOpen={setCycleDeleteModal} data={cycle} />
|
||||
<DeleteCycleModal
|
||||
isOpen={cycleDeleteModal}
|
||||
setIsOpen={setCycleDeleteModal}
|
||||
data={cycle}
|
||||
user={user}
|
||||
/>
|
||||
<div
|
||||
className={`fixed top-[66px] ${
|
||||
isOpen ? "right-0" : "-right-[24rem]"
|
||||
@ -447,7 +453,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<Image
|
||||
<img
|
||||
src={cycle.owned_by.avatar}
|
||||
height={12}
|
||||
width={12}
|
||||
|
@ -1,23 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// headless ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { SingleProgressStats } from "components/core";
|
||||
// ui
|
||||
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { AssigneesList, Avatar } from "components/ui/avatar";
|
||||
import { SingleProgressStats } from "components/core";
|
||||
|
||||
import { AssigneesList } from "components/ui/avatar";
|
||||
// icons
|
||||
import { CalendarDaysIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
@ -41,24 +37,14 @@ import {
|
||||
} from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import {
|
||||
CompletedCyclesResponse,
|
||||
CurrentAndUpcomingCyclesResponse,
|
||||
DraftCyclesResponse,
|
||||
ICycle,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_DRAFT_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { ICycle } from "types";
|
||||
|
||||
type TSingleStatProps = {
|
||||
cycle: ICycle;
|
||||
handleEditCycle: () => void;
|
||||
handleDeleteCycle: () => void;
|
||||
handleAddToFavorites: () => void;
|
||||
handleRemoveFromFavorites: () => void;
|
||||
isCompleted?: boolean;
|
||||
};
|
||||
|
||||
@ -94,6 +80,8 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
cycle,
|
||||
handleEditCycle,
|
||||
handleDeleteCycle,
|
||||
handleAddToFavorites,
|
||||
handleRemoveFromFavorites,
|
||||
isCompleted = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -105,142 +93,6 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
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<CurrentAndUpcomingCyclesResponse>(
|
||||
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<CompletedCyclesResponse>(
|
||||
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<DraftCyclesResponse>(
|
||||
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<CurrentAndUpcomingCyclesResponse>(
|
||||
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<CompletedCyclesResponse>(
|
||||
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<DraftCyclesResponse>(
|
||||
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<TSingleStatProps> = ({
|
||||
<div className="w-16">Creator:</div>
|
||||
<div className="flex items-center gap-2.5 text-brand-secondary">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<Image
|
||||
<img
|
||||
src={cycle.owned_by.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
|
@ -4,17 +4,12 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
|
||||
|
||||
// icons
|
||||
import { CalendarDaysIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
@ -32,25 +27,14 @@ import {
|
||||
} from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import {
|
||||
CompletedCyclesResponse,
|
||||
CurrentAndUpcomingCyclesResponse,
|
||||
DraftCyclesResponse,
|
||||
ICycle,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_DRAFT_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { type } from "os";
|
||||
import { ICycle } from "types";
|
||||
|
||||
type TSingleStatProps = {
|
||||
cycle: ICycle;
|
||||
handleEditCycle: () => void;
|
||||
handleDeleteCycle: () => void;
|
||||
handleAddToFavorites: () => void;
|
||||
handleRemoveFromFavorites: () => void;
|
||||
isCompleted?: boolean;
|
||||
};
|
||||
|
||||
@ -128,6 +112,8 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
cycle,
|
||||
handleEditCycle,
|
||||
handleDeleteCycle,
|
||||
handleAddToFavorites,
|
||||
handleRemoveFromFavorites,
|
||||
isCompleted = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -139,142 +125,6 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
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<CurrentAndUpcomingCyclesResponse>(
|
||||
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<CompletedCyclesResponse>(
|
||||
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<DraftCyclesResponse>(
|
||||
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<CurrentAndUpcomingCyclesResponse>(
|
||||
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<CompletedCyclesResponse>(
|
||||
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<DraftCyclesResponse>(
|
||||
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<TSingleStatProps> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col border-b border-brand-base text-xs hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col text-xs hover:bg-brand-surface-2">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||
<a className="w-full">
|
||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
||||
@ -394,7 +244,7 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
|
||||
<div className="flex items-center gap-2.5 text-brand-secondary">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<Image
|
||||
<img
|
||||
src={cycle.owned_by.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
|
@ -10,16 +10,16 @@ import { Dialog, Transition } from "@headlessui/react";
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
//icons
|
||||
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { ContrastIcon, CyclesIcon, ExclamationIcon, TransferIcon } from "components/icons";
|
||||
import { ContrastIcon, ExclamationIcon, TransferIcon } from "components/icons";
|
||||
// fetch-key
|
||||
import { CYCLE_INCOMPLETE_LIST, CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
|
||||
import { CYCLE_ISSUES_WITH_PARAMS, INCOMPLETE_CYCLES_LIST } from "constants/fetch-keys";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
//helper
|
||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -57,9 +57,14 @@ export const TransferIssuesModal: React.FC<Props> = ({ 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
|
||||
);
|
||||
|
||||
|
@ -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<FormValues> = {
|
||||
value6: "",
|
||||
};
|
||||
|
||||
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => {
|
||||
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen, user }) => {
|
||||
const {
|
||||
register,
|
||||
formState: { isSubmitting },
|
||||
@ -73,7 +74,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ 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<Props> = ({ 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));
|
||||
|
@ -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<Props> = ({
|
||||
user,
|
||||
estimate,
|
||||
editEstimate,
|
||||
handleEstimateDelete,
|
||||
@ -52,7 +54,7 @@ export const SingleEstimate: React.FC<Props> = ({
|
||||
}, false);
|
||||
|
||||
await projectService
|
||||
.updateProject(workspaceSlug as string, projectId as string, payload)
|
||||
.updateProject(workspaceSlug as string, projectId as string, payload, user)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
|
@ -49,7 +49,10 @@ export const GanttChartBlocks: FC<{
|
||||
width: `${block?.position?.width}px`,
|
||||
}}
|
||||
>
|
||||
{blockRender({ ...block?.data })}
|
||||
{blockRender({
|
||||
...block?.data,
|
||||
infoToggle: block?.infoToggle ? true : false,
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 relative w-0 h-0 flex items-center invisible group-hover:visible whitespace-nowrap">
|
||||
|
@ -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<Props> = ({ isOpen, handleClose, data }) => {
|
||||
export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data, user }) => {
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [confirmDeleteImport, setConfirmDeleteImport] = useState(false);
|
||||
|
||||
@ -45,7 +46,7 @@ export const DeleteImportModal: React.FC<Props> = ({ 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",
|
||||
|
@ -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<Props> = ({ user }) => {
|
||||
const [currentStep, setCurrentStep] = useState<IIntegrationData>({
|
||||
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));
|
||||
|
@ -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<Props> = ({ collaborator, index, users,
|
||||
<div className="grid grid-cols-3 items-center gap-2 rounded-md bg-brand-surface-2 px-2 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-8 w-8 flex-shrink-0 rounded">
|
||||
<Image
|
||||
<img
|
||||
src={collaborator.avatar_url}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded"
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded"
|
||||
alt={`${collaborator.login} GitHub user`}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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}
|
||||
/>
|
||||
<div className="h-full space-y-2">
|
||||
{!provider && (
|
||||
@ -156,8 +161,8 @@ const IntegrationGuide = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{provider && provider === "github" && <GithubImporterRoot />}
|
||||
{provider && provider === "jira" && <JiraImporterRoot />}
|
||||
{provider && provider === "github" && <GithubImporterRoot user={user} />}
|
||||
{provider && provider === "jira" && <JiraImporterRoot user={user} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -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<Props> = ({ user }) => {
|
||||
const [currentStep, setCurrentStep] = useState<IJiraIntegrationData>({
|
||||
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`);
|
||||
|
@ -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<Props> = ({ 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 !== "" ? (
|
||||
<Image
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.first_name}
|
||||
height={24}
|
||||
|
@ -14,7 +14,7 @@ import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Loader, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
import type { IIssueComment } from "types";
|
||||
import type { ICurrentUserResponse, IIssueComment } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
@ -40,7 +40,11 @@ const defaultValues: Partial<IIssueComment> = {
|
||||
comment_html: "",
|
||||
};
|
||||
|
||||
export const AddComment: React.FC = () => {
|
||||
type Props = {
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const AddComment: React.FC<Props> = ({ 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);
|
||||
|
@ -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<Props> = ({ comment, onSubmit, handleCommentD
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
<Image
|
||||
<img
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={comment.actor_detail.first_name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-brand-base"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user