diff --git a/apiserver/.env.example b/apiserver/.env.example index 15056f072..8a7c76ffa 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -3,15 +3,19 @@ DJANGO_SETTINGS_MODULE="plane.settings.production" DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane # Cache REDIS_URL=redis://redis:6379/ -# SMPT +# SMTP EMAIL_HOST="" EMAIL_HOST_USER="" EMAIL_HOST_PASSWORD="" -# AWS +EMAIL_PORT="587" +EMAIL_USE_TLS="1" +EMAIL_FROM="Team Plane " +# AWS AWS_REGION="" AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_S3_BUCKET_NAME="" +AWS_S3_ENDPOINT_URL="" # FE WEB_URL="localhost/" # OAUTH @@ -21,4 +25,4 @@ DISABLE_COLLECTSTATIC=1 DOCKERIZED=1 # GPT Envs OPENAI_API_KEY=0 -GPT_ENGINE=0 \ No newline at end of file +GPT_ENGINE=0 diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 0e27ce665..2c202a1c0 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -79,6 +79,7 @@ from plane.api.views import ( ## End Issues # States StateViewSet, + StateDeleteIssueCheckEndpoint, ## End States # Estimates EstimateViewSet, @@ -146,6 +147,9 @@ from plane.api.views import ( # Gpt GPTIntegrationEndpoint, ## End Gpt + # Release Notes + ReleaseNotesEndpoint, + ## End Release Notes ) @@ -506,6 +510,11 @@ urlpatterns = [ ), name="project-state", ), + path( + "workspaces//projects//states//", + StateDeleteIssueCheckEndpoint.as_view(), + name="state-delete-check", + ), # End States ## # States path( @@ -557,6 +566,11 @@ urlpatterns = [ ProjectEstimatePointEndpoint.as_view(), name="project-estimate-points", ), + path( + "workspaces//projects//estimates/bulk-estimate-points/", + BulkEstimatePointEndpoint.as_view(), + name="bulk-create-estimate-points", + ), path( "workspaces//projects//estimates//bulk-estimate-points/", BulkEstimatePointEndpoint.as_view(), @@ -1284,4 +1298,11 @@ urlpatterns = [ name="importer", ), ## End Gpt + # Release Notes + path( + "release-notes/", + ReleaseNotesEndpoint.as_view(), + name="release-notes", + ), + ## End Release Notes ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 82eb49e44..18809cd9d 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -42,7 +42,7 @@ from .workspace import ( UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, ) -from .state import StateViewSet +from .state import StateViewSet, StateDeleteIssueCheckEndpoint from .shortcut import ShortCutViewSet from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet from .cycle import ( @@ -138,3 +138,6 @@ from .estimate import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, ) + + +from .release import ReleaseNotesEndpoint diff --git a/apiserver/plane/api/views/estimate.py b/apiserver/plane/api/views/estimate.py index 96d0ed1a4..99374282d 100644 --- a/apiserver/plane/api/views/estimate.py +++ b/apiserver/plane/api/views/estimate.py @@ -146,11 +146,13 @@ class BulkEstimatePointEndpoint(BaseAPIView): ProjectEntityPermission, ] - def post(self, request, slug, project_id, estimate_id): + def post(self, request, slug, project_id): try: - estimate = Estimate.objects.get( - pk=estimate_id, workspace__slug=slug, project=project_id - ) + if not request.data.get("estimate", False): + return Response( + {"error": "Estimate is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) estimate_points = request.data.get("estimate_points", []) @@ -160,6 +162,18 @@ class BulkEstimatePointEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) + estimate_serializer = EstimateSerializer(data=request.data.get("estimate")) + if not estimate_serializer.is_valid(): + return Response( + estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + try: + estimate = estimate_serializer.save(project_id=project_id) + except IntegrityError: + return Response( + {"errror": "Estimate with the name already exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) estimate_points = EstimatePoint.objects.bulk_create( [ EstimatePoint( @@ -178,9 +192,17 @@ class BulkEstimatePointEndpoint(BaseAPIView): ignore_conflicts=True, ) - serializer = EstimatePointSerializer(estimate_points, many=True) + estimate_point_serializer = EstimatePointSerializer( + estimate_points, many=True + ) - return Response(serializer.data, status=status.HTTP_200_OK) + return Response( + { + "estimate": estimate_serializer.data, + "estimate_points": estimate_point_serializer.data, + }, + status=status.HTTP_200_OK, + ) except Estimate.DoesNotExist: return Response( {"error": "Estimate does not exist"}, @@ -212,7 +234,6 @@ class BulkEstimatePointEndpoint(BaseAPIView): estimate_id=estimate_id, ) - print(estimate_points) updated_estimate_points = [] for estimate_point in estimate_points: # Find the data for that estimate point @@ -238,7 +259,7 @@ class BulkEstimatePointEndpoint(BaseAPIView): {"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: - print(e) + capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 1f604d271..c0f54bcdb 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -1,13 +1,14 @@ # Python imports import json import random -from itertools import groupby, chain +from itertools import chain # Django imports -from django.db.models import Prefetch, OuterRef, Func, F, Q +from django.db.models import Prefetch, OuterRef, Func, F, Q, Count 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 # Third Party imports from rest_framework.response import Response @@ -46,6 +47,7 @@ from plane.db.models import ( Label, IssueLink, IssueAttachment, + State, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -590,10 +592,33 @@ class SubIssuesEndpoint(BaseAPIView): .prefetch_related("labels") ) - serializer = IssueLiteSerializer(sub_issues, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + state_distribution = ( + State.objects.filter(workspace__slug=slug, project_id=project_id) + .annotate( + state_count=Count( + "state_issue", + filter=Q(state_issue__parent_id=issue_id), + ) + ) + .order_by("group") + .values("group", "state_count") + ) + + result = {item["group"]: item["state_count"] for item in state_distribution} + + serializer = IssueLiteSerializer( + sub_issues, + many=True, + ) + return Response( + { + "sub_issues": serializer.data, + "state_distribution": result, + }, + status=status.HTTP_200_OK, + ) except Exception as e: - capture_exception(e) + print(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/apiserver/plane/api/views/release.py b/apiserver/plane/api/views/release.py new file mode 100644 index 000000000..de827c896 --- /dev/null +++ b/apiserver/plane/api/views/release.py @@ -0,0 +1,21 @@ +# Third party imports +from rest_framework.response import Response +from rest_framework import status +from sentry_sdk import capture_exception + +# Module imports +from .base import BaseAPIView +from plane.utils.integrations.github import get_release_notes + + +class ReleaseNotesEndpoint(BaseAPIView): + def get(self, request): + try: + release_notes = get_release_notes() + return Response(release_notes, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 048ac4a6a..f1e409e14 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -11,10 +11,10 @@ from sentry_sdk import capture_exception # Module imports -from . import BaseViewSet +from . import BaseViewSet, BaseAPIView from plane.api.serializers import StateSerializer from plane.api.permissions import ProjectEntityPermission -from plane.db.models import State +from plane.db.models import State, Issue class StateViewSet(BaseViewSet): @@ -53,7 +53,10 @@ class StateViewSet(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 list(self, request, slug, project_id): try: @@ -85,7 +88,37 @@ class StateViewSet(BaseViewSet): {"error": "Default state cannot be deleted"}, status=False ) + # Check for any issues in the state + issue_exist = Issue.objects.filter(state=pk).exists() + + if issue_exist: + return Response( + { + "error": "The state is not empty, only empty states can be deleted" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + state.delete() return Response(status=status.HTTP_204_NO_CONTENT) except State.DoesNotExist: return Response({"error": "State does not exists"}, status=status.HTTP_404) + + +class StateDeleteIssueCheckEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + def get(self, request, slug, project_id, pk): + try: + issue_count = Issue.objects.filter( + state=pk, workspace__slug=slug, project_id=project_id + ).count() + return Response({"issue_count": issue_count}, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/bgtasks/email_verification_task.py b/apiserver/plane/bgtasks/email_verification_task.py index ee4680e53..1da3a7510 100644 --- a/apiserver/plane/bgtasks/email_verification_task.py +++ b/apiserver/plane/bgtasks/email_verification_task.py @@ -2,6 +2,7 @@ from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils.html import strip_tags +from django.conf import settings # Third party imports from celery import shared_task @@ -20,7 +21,7 @@ def email_verification(first_name, email, token, current_site): realtivelink = "/request-email-verification/" + "?token=" + str(token) abs_url = "http://" + current_site + realtivelink - from_email_string = f"Team Plane " + from_email_string = settings.EMAIL_FROM subject = f"Verify your Email!" diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index 4598e5f2f..f13f1b89a 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -2,6 +2,7 @@ from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils.html import strip_tags +from django.conf import settings # Third party imports from celery import shared_task @@ -18,7 +19,7 @@ def forgot_password(first_name, email, uidb64, token, current_site): realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/" abs_url = "http://" + current_site + realtivelink - from_email_string = f"Team Plane " + from_email_string = settings.EMAIL_FROM subject = f"Verify your Email!" diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index 89554dcca..ea97b0fb8 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -2,6 +2,7 @@ from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils.html import strip_tags +from django.conf import settings # Third party imports from celery import shared_task @@ -14,7 +15,7 @@ def magic_link(email, key, token, current_site): realtivelink = f"/magic-sign-in/?password={token}&key={key}" abs_url = "http://" + current_site + realtivelink - from_email_string = f"Team Plane " + from_email_string = settings.EMAIL_FROM subject = f"Login for Plane" diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index 18e539970..1c3597120 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -2,6 +2,7 @@ from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils.html import strip_tags +from django.conf import settings # Third party imports from celery import shared_task @@ -22,7 +23,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 - from_email_string = f"Team Plane " + from_email_string = settings.EMAIL_FROM subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane" diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index c6e69689b..0ce32eee0 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -27,7 +27,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): ) abs_url = "http://" + current_site + realtivelink - from_email_string = f"Team Plane " + from_email_string = settings.EMAIL_FROM subject = f"{invitor or email} invited you to join {workspace.name} on Plane" diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 334ec3e13..5a4f487c1 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -109,7 +109,7 @@ def send_welcome_email(sender, instance, created, **kwargs): if created and not instance.is_bot: first_name = instance.first_name.capitalize() to_email = instance.email - from_email_string = f"Team Plane " + from_email_string = settings.EMAIL_FROM subject = f"Welcome to Plane ✈️!" diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index c144eeb0b..f5bff248b 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -174,11 +174,12 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" # Host for sending e-mail. EMAIL_HOST = os.environ.get("EMAIL_HOST") # Port for sending e-mail. -EMAIL_PORT = 587 +EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587)) # Optional SMTP authentication information for EMAIL_HOST. EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") -EMAIL_USE_TLS = True +EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "1") == "1" +EMAIL_FROM = os.environ.get("EMAIL_FROM", "Team Plane ") SIMPLE_JWT = { @@ -210,4 +211,4 @@ SIMPLE_JWT = { CELERY_TIMEZONE = TIME_ZONE CELERY_TASK_SERIALIZER = 'json' -CELERY_ACCEPT_CONTENT = ['application/json'] \ No newline at end of file +CELERY_ACCEPT_CONTENT = ['application/json'] diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index c3bf65588..e03a0b822 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -83,3 +83,6 @@ LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False) CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL") CELERY_BROKER_URL = os.environ.get("REDIS_URL") + + +GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) \ No newline at end of file diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index d8f2a8bb7..e58736472 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -105,7 +105,7 @@ if ( AWS_S3_ADDRESSING_STYLE = "auto" # The full URL to the S3 endpoint. Leave blank to use the default region URL. - AWS_S3_ENDPOINT_URL = "" + AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "") # A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator. AWS_S3_KEY_PREFIX = "" @@ -240,7 +240,9 @@ SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False) redis_url = os.environ.get("REDIS_URL") -broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" +broker_url = ( + f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" +) if DOCKERIZED: CELERY_BROKER_URL = REDIS_URL @@ -248,3 +250,5 @@ if DOCKERIZED: else: CELERY_RESULT_BACKEND = broker_url CELERY_BROKER_URL = broker_url + +GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index 384116ba3..b43327c09 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -203,4 +203,6 @@ redis_url = os.environ.get("REDIS_URL") broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" CELERY_RESULT_BACKEND = broker_url -CELERY_BROKER_URL = broker_url \ No newline at end of file +CELERY_BROKER_URL = broker_url + +GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) diff --git a/apiserver/plane/utils/integrations/github.py b/apiserver/plane/utils/integrations/github.py index d9185cb10..d9aecece1 100644 --- a/apiserver/plane/utils/integrations/github.py +++ b/apiserver/plane/utils/integrations/github.py @@ -5,6 +5,7 @@ from urllib.parse import urlparse, parse_qs from datetime import datetime, timedelta from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.backends import default_backend +from django.conf import settings def get_jwt_token(): @@ -128,3 +129,24 @@ def get_github_repo_details(access_tokens_url, owner, repo): ).json() return open_issues, total_labels, collaborators + + +def get_release_notes(): + token = settings.GITHUB_ACCESS_TOKEN + + if token: + headers = { + "Authorization": "Bearer " + str(token), + "Accept": "application/vnd.github.v3+json", + } + else: + headers = { + "Accept": "application/vnd.github.v3+json", + } + url = "https://api.github.com/repos/makeplane/plane/releases?per_page=5&page=1" + response = requests.get(url, headers=headers) + + if response.status_code != 200: + return {"error": "Unable to render information from Github Repository"} + + return response.json() diff --git a/apps/app/.env.example b/apps/app/.env.example index 78ce84a64..9e41ba88d 100644 --- a/apps/app/.env.example +++ b/apps/app/.env.example @@ -1,5 +1,6 @@ # Replace with your instance Public IP # NEXT_PUBLIC_API_BASE_URL = "http://localhost" +NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= NEXT_PUBLIC_GOOGLE_CLIENTID="" NEXT_PUBLIC_GITHUB_APP_NAME="" NEXT_PUBLIC_GITHUB_ID="" @@ -8,4 +9,4 @@ NEXT_PUBLIC_ENABLE_OAUTH=0 NEXT_PUBLIC_ENABLE_SENTRY=0 NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0 NEXT_PUBLIC_TRACK_EVENTS=0 -NEXT_PUBLIC_SLACK_CLIENT_ID="" \ No newline at end of file +NEXT_PUBLIC_SLACK_CLIENT_ID="" diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index 5e4c49b1a..389153d60 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -141,7 +141,7 @@ export const EmailCodeForm = ({ onSuccess }: any) => { diff --git a/apps/app/components/auth-screens/not-authorized-view.tsx b/apps/app/components/auth-screens/not-authorized-view.tsx index 37c07e8de..054e5bfa3 100644 --- a/apps/app/components/auth-screens/not-authorized-view.tsx +++ b/apps/app/components/auth-screens/not-authorized-view.tsx @@ -36,16 +36,16 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { alt="ProjectSettingImg" /> -

+

Oops! You are not authorized to view this page

-
+
{user ? (

You have signed in as {user.email}.
- Sign in + Sign in {" "} with different account that has access to this page.

@@ -53,7 +53,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => {

You need to{" "} - Sign in + Sign in {" "} with an account that has access to this page.

diff --git a/apps/app/components/breadcrumbs/index.tsx b/apps/app/components/breadcrumbs/index.tsx index 0a2d36e1f..98944fe37 100644 --- a/apps/app/components/breadcrumbs/index.tsx +++ b/apps/app/components/breadcrumbs/index.tsx @@ -16,7 +16,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
-
- +
+ = ({ isOpen, setIsOpen }) => {
-

{shortcut.description}

+

{shortcut.description}

{shortcut.keys.split(",").map((key, index) => ( {key === "Ctrl" ? ( - + ) : ( - + {key === "Ctrl" ? : key} )} @@ -145,7 +145,7 @@ export const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { )) ) : (
-

+

No shortcuts found for{" "} {`"`} @@ -162,16 +162,16 @@ export const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => {

{shortcuts.map(({ keys, description }, index) => (
-

{description}

+

{description}

{keys.split(",").map((key, index) => ( {key === "Ctrl" ? ( - + ) : ( - + {key === "Ctrl" ? : key} )} diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx index f476b76a5..19d0e7636 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -81,7 +81,7 @@ export const AllBoards: React.FC = ({ return (
{currentState && @@ -92,7 +92,7 @@ export const AllBoards: React.FC = ({ : addSpaceIfCamelCase(singleGroup)}
- 0 + 0
); })} diff --git a/apps/app/components/core/board-view/board-header.tsx b/apps/app/components/core/board-view/board-header.tsx index f209ba6e2..6fa8f68f3 100644 --- a/apps/app/components/core/board-view/board-header.tsx +++ b/apps/app/components/core/board-view/board-header.tsx @@ -123,8 +123,8 @@ export const BoardHeader: React.FC = ({ return (
@@ -145,7 +145,7 @@ export const BoardHeader: React.FC = ({ {groupedByIssues?.[groupTitle].length ?? 0} @@ -155,7 +155,7 @@ export const BoardHeader: React.FC = ({
))}
-
+
{monthOptions.map((month) => (
-
+
+ {isMonthlyView ? "Monthly" : "Weekly"}
+ ) : ( +
+ +
); }; diff --git a/apps/app/components/core/custom-theme-form.tsx b/apps/app/components/core/custom-theme-form.tsx new file mode 100644 index 000000000..ed1009468 --- /dev/null +++ b/apps/app/components/core/custom-theme-form.tsx @@ -0,0 +1,267 @@ +import { useEffect, useState } from "react"; + +// react-hook-form +import { useForm } from "react-hook-form"; +// ui +import { Input, PrimaryButton, SecondaryButton } from "components/ui"; + +const defaultValues = { + palette: "", +}; + +export const ThemeForm: React.FC = ({ handleFormSubmit, handleClose, status, data }) => { + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + reset, + } = useForm({ + defaultValues, + }); + const [darkPalette, setDarkPalette] = useState(false); + + const handleUpdateTheme = async (formData: any) => { + await handleFormSubmit({ ...formData, darkPalette }); + + reset({ + ...defaultValues, + }); + }; + + useEffect(() => { + reset({ + ...defaultValues, + ...data, + }); + }, [data, reset]); + + // --color-bg-base: 25, 27, 27; + // --color-bg-surface-1: 31, 32, 35; + // --color-bg-surface-2: 39, 42, 45; + + // --color-border: 46, 50, 52; + // --color-bg-sidebar: 19, 20, 22; + // --color-accent: 60, 133, 217; + + // --color-text-base: 255, 255, 255; + // --color-text-secondary: 142, 148, 146; + + return ( + +
+

Customize your theme

+
+
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
setDarkPalette((prevData) => !prevData)} + > + Dark palette + +
+
+
+
+ Cancel + + {status + ? isSubmitting + ? "Updating Theme..." + : "Update Theme" + : isSubmitting + ? "Creating Theme..." + : "Set Theme"} + +
+ + ); +}; diff --git a/apps/app/components/core/custom-theme-modal.tsx b/apps/app/components/core/custom-theme-modal.tsx new file mode 100644 index 000000000..d46d17d28 --- /dev/null +++ b/apps/app/components/core/custom-theme-modal.tsx @@ -0,0 +1,65 @@ +import React from "react"; + +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// components +import { ThemeForm } from "./custom-theme-form"; +// helpers +import { applyTheme } from "helpers/theme.helper"; +// fetch-keys + +type Props = { + isOpen: boolean; + handleClose: () => void; +}; + +export const CustomThemeModal: React.FC = ({ isOpen, handleClose }) => { + const onClose = () => { + handleClose(); + }; + + const handleFormSubmit = async (formData: any) => { + applyTheme(formData.palette, formData.darkPalette); + onClose(); + }; + + return ( + + + +
+ + +
+
+ + + + + +
+
+
+
+ ); +}; diff --git a/apps/app/components/core/existing-issues-list-modal.tsx b/apps/app/components/core/existing-issues-list-modal.tsx index 2e255ae19..2d5ac37dc 100644 --- a/apps/app/components/core/existing-issues-list-modal.tsx +++ b/apps/app/components/core/existing-issues-list-modal.tsx @@ -130,7 +130,7 @@ export const ExistingIssuesListModal: React.FC = ({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - +
= ({
)} - +
-

+

Commented {timeAgo(activity.created_at)}

@@ -244,7 +244,7 @@ export const Feeds: React.FC = ({ activities }) => ( editable={false} onBlur={() => ({})} noBorder - customClassName="text-xs bg-gray-100" + customClassName="text-xs bg-brand-surface-1" />
@@ -259,7 +259,7 @@ export const Feeds: React.FC = ({ activities }) => (
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
-
+
{activity.field ? ( activityDetails[activity.field as keyof typeof activityDetails]?.icon ) : activity.actor_detail.avatar && @@ -292,7 +292,7 @@ export const Feeds: React.FC = ({ activities }) => (
-
+
{activity.actor_detail.first_name} {activity.actor_detail.is_bot @@ -300,7 +300,7 @@ export const Feeds: React.FC = ({ activities }) => ( : " " + activity.actor_detail.last_name} {action} - {value} + {value} {timeAgo(activity.created_at)}
diff --git a/apps/app/components/core/filter-list.tsx b/apps/app/components/core/filter-list.tsx index 08d1e8062..c42b7501b 100644 --- a/apps/app/components/core/filter-list.tsx +++ b/apps/app/components/core/filter-list.tsx @@ -57,9 +57,9 @@ export const FilterList: React.FC = ({ filters, setFilters }) => { return (
- + {replaceUnderscoreIfSnakeCase(key)}: {filters[key as keyof IIssueFilterOptions] === null || @@ -131,7 +131,7 @@ export const FilterList: React.FC = ({ filters, setFilters }) => { ? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100" : priority === "low" ? "bg-green-100 text-green-500 hover:bg-green-100" - : "bg-gray-100 text-gray-700 hover:bg-gray-100" + : "bg-brand-surface-1 text-gray-700 hover:bg-brand-surface-1" }`} > {getPriorityIcon(priority)} @@ -339,7 +339,7 @@ export const FilterList: React.FC = ({ filters, setFilters }) => { created_by: null, }) } - className="flex items-center gap-x-1 rounded-full border bg-white px-3 py-1.5 text-xs" + className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs" > Clear all filters diff --git a/apps/app/components/core/gpt-assistant-modal.tsx b/apps/app/components/core/gpt-assistant-modal.tsx index b50b1e0f8..37104e30f 100644 --- a/apps/app/components/core/gpt-assistant-modal.tsx +++ b/apps/app/components/core/gpt-assistant-modal.tsx @@ -121,7 +121,7 @@ export const GptAssistantModal: React.FC = ({ return (
diff --git a/apps/app/components/core/image-picker-popover.tsx b/apps/app/components/core/image-picker-popover.tsx index a6f2efb72..9a3d5ff2f 100644 --- a/apps/app/components/core/image-picker-popover.tsx +++ b/apps/app/components/core/image-picker-popover.tsx @@ -65,7 +65,7 @@ export const ImagePickerPopover: React.FC = ({ label, value, onChange }) return ( setIsOpen((prev) => !prev)} > {label} @@ -79,16 +79,16 @@ export const ImagePickerPopover: React.FC = ({ label, value, onChange }) leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - -
+ +
- + {tabOptions.map((tab) => ( `rounded py-1 px-4 text-center text-sm outline-none transition-colors ${ - selected ? "bg-theme text-white" : "text-black" + selected ? "bg-brand-accent text-white" : "text-brand-base" }` } > diff --git a/apps/app/components/core/image-upload-modal.tsx b/apps/app/components/core/image-upload-modal.tsx index 28ff260d4..ba41f3efa 100644 --- a/apps/app/components/core/image-upload-modal.tsx +++ b/apps/app/components/core/image-upload-modal.tsx @@ -110,7 +110,7 @@ export const ImageUploadModal: React.FC = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
@@ -124,9 +124,9 @@ export const ImageUploadModal: React.FC = ({ leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - +
- + Upload Image
@@ -135,7 +135,7 @@ export const ImageUploadModal: React.FC = ({ {...getRootProps()} className={`relative block h-80 w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${ (image === null && isDragActive) || !value - ? "border-2 border-dashed border-gray-300 hover:border-gray-400" + ? "border-2 border-dashed border-brand-base hover:border-gray-400" : "" }`} > @@ -143,7 +143,7 @@ export const ImageUploadModal: React.FC = ({ <> @@ -157,7 +157,7 @@ export const ImageUploadModal: React.FC = ({ ) : ( <> - + {isDragActive ? "Drop image here to upload" : "Drag & drop image here"} diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index 41a71d965..eb547578c 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -11,3 +11,4 @@ export * from "./link-modal"; export * from "./image-picker-popover"; export * from "./filter-list"; export * from "./feeds"; +export * from "./theme-switch"; diff --git a/apps/app/components/core/issues-view-filter.tsx b/apps/app/components/core/issues-view-filter.tsx index 4b4d15e09..49bf5cf6b 100644 --- a/apps/app/components/core/issues-view-filter.tsx +++ b/apps/app/components/core/issues-view-filter.tsx @@ -12,8 +12,12 @@ import { SelectFilters } from "components/views"; // ui import { CustomMenu } from "components/ui"; // icons -import { ChevronDownIcon, ListBulletIcon, CalendarDaysIcon } from "@heroicons/react/24/outline"; -import { Squares2X2Icon } from "@heroicons/react/20/solid"; +import { + ChevronDownIcon, + ListBulletIcon, + Squares2X2Icon, + CalendarDaysIcon, +} from "@heroicons/react/24/outline"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // types @@ -53,30 +57,30 @@ export const IssuesFilterView: React.FC = () => {
{ {({ open }) => ( <> View @@ -130,55 +134,59 @@ export const IssuesFilterView: React.FC = () => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - -
+ +
+ {issueView !== "calendar" && ( + <> +
+

Group by

+ option.key === groupByProperty) + ?.name ?? "Select" + } + width="lg" + > + {GROUP_BY_OPTIONS.map((option) => + issueView === "kanban" && option.key === null ? null : ( + setGroupByProperty(option.key)} + > + {option.name} + + ) + )} + +
+
+

Order by

+ option.key === orderBy)?.name ?? + "Select" + } + width="lg" + > + {ORDER_BY_OPTIONS.map((option) => + groupByProperty === "priority" && option.key === "priority" ? null : ( + { + setOrderBy(option.key); + }} + > + {option.name} + + ) + )} + +
+ + )}
-

Group by

- option.key === groupByProperty)?.name ?? - "Select" - } - width="lg" - > - {GROUP_BY_OPTIONS.map((option) => - issueView === "kanban" && option.key === null ? null : ( - setGroupByProperty(option.key)} - > - {option.name} - - ) - )} - -
-
-

Order by

- option.key === orderBy)?.name ?? - "Select" - } - width="lg" - > - {ORDER_BY_OPTIONS.map((option) => - groupByProperty === "priority" && option.key === "priority" ? null : ( - { - setOrderBy(option.key); - }} - > - {option.name} - - ) - )} - -
-
-

Issue type

+

Issue type

option.key === filters.type) @@ -200,62 +208,69 @@ export const IssuesFilterView: React.FC = () => { ))}
-
-

Show empty states

- -
-
- - -
-
-
-

Display Properties

-
- {Object.keys(properties).map((key) => { - if (key === "estimate" && !isEstimateActive) return null; - return ( + {issueView !== "calendar" && ( + <> +
+

Show empty states

- ); - })} -
+
+
+ + +
+ + )}
+ {issueView !== "calendar" && ( +
+

Display Properties

+
+ {Object.keys(properties).map((key) => { + if (key === "estimate" && !isEstimateActive) return null; + + return ( + + ); + })} +
+
+ )}
diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 22261de06..240b7fbd0 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -280,6 +280,17 @@ export const IssuesView: React.FC = ({ [setCreateIssueModal, setPreloadedData, selectedGroup] ); + const addIssueToDate = useCallback( + (date: string) => { + setCreateIssueModal(true); + setPreloadedData({ + target_date: date, + actionType: "createIssue", + }); + }, + [setCreateIssueModal, setPreloadedData] + ); + const makeIssueCopy = useCallback( (issue: IIssue) => { setCreateIssueModal(true); @@ -385,49 +396,48 @@ export const IssuesView: React.FC = ({ handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> - {issueView !== "calendar" && ( - <> -
- - {areFiltersApplied && ( - { - if (viewId) { - setFilters({}, true); - setToastAlert({ - title: "View updated", - message: "Your view has been updated", - type: "success", - }); - } else - setCreateViewModal({ - query: filters, - }); - }} - className="flex items-center gap-2 text-sm" - > - {!viewId && } - {viewId ? "Update" : "Save"} view - - )} -
+ <> +
+ {areFiltersApplied && ( -
+ { + if (viewId) { + setFilters({}, true); + setToastAlert({ + title: "View updated", + message: "Your view has been updated", + type: "success", + }); + } else + setCreateViewModal({ + query: filters, + }); + }} + className="flex items-center gap-2 text-sm" + > + {!viewId && } + {viewId ? "Update" : "Save"} view + )} - - )} +
+ {areFiltersApplied && ( +
+ )} + + {(provided, snapshot) => (
= ({ userAuth={memberRole} /> ) : ( - + )} ) : type === "issue" ? ( @@ -502,8 +512,8 @@ export const IssuesView: React.FC = ({ title="Create a new issue" description={ - Use
C
shortcut to - create a new issue + Use
C
{" "} + shortcut to create a new issue
} Icon={PlusIcon} diff --git a/apps/app/components/core/link-modal.tsx b/apps/app/components/core/link-modal.tsx index 7fd19dc8a..3c22c2b1b 100644 --- a/apps/app/components/core/link-modal.tsx +++ b/apps/app/components/core/link-modal.tsx @@ -56,7 +56,7 @@ export const LinkModal: React.FC = ({ isOpen, handleClose, onFormSubmit } leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
@@ -70,11 +70,11 @@ export const LinkModal: React.FC = ({ isOpen, handleClose, onFormSubmit } leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - +
- + Add Link
diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx index 4fa518769..0d34adf9e 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -44,7 +44,6 @@ import { MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, } from "constants/fetch-keys"; -import { DIVIDER } from "@blueprintjs/core/lib/esm/common/classes"; type Props = { type?: string; @@ -217,7 +216,7 @@ export const SingleListIssue: React.FC = ({
{ e.preventDefault(); setContextMenu(true); @@ -231,13 +230,15 @@ export const SingleListIssue: React.FC = ({ tooltipHeading="Issue ID" tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`} > - + {issue.project_detail?.identifier}-{issue.sequence_id} )} - {truncateText(issue.name, 50)} + + {truncateText(issue.name, 50)} + @@ -267,7 +268,7 @@ export const SingleListIssue: React.FC = ({ /> )} {properties.sub_issue_count && ( -
+
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
)} @@ -276,7 +277,7 @@ export const SingleListIssue: React.FC = ({ {issue.label_details.map((label) => ( = ({ /> )} {properties.link && ( -
- +
+
{issue.link_count} @@ -318,10 +319,10 @@ export const SingleListIssue: React.FC = ({
)} {properties.attachment_count && ( -
- +
+
- + {issue.attachment_count}
diff --git a/apps/app/components/core/list-view/single-list.tsx b/apps/app/components/core/list-view/single-list.tsx index 32880a1a6..c39e6a56b 100644 --- a/apps/app/components/core/list-view/single-list.tsx +++ b/apps/app/components/core/list-view/single-list.tsx @@ -130,35 +130,31 @@ export const SingleList: React.FC = ({ }; return ( - + {({ open }) => ( -
-
+
+
{selectedGroup !== null && (
{getGroupIcon()}
)} {selectedGroup !== null ? ( -

+

{getGroupTitle()}

) : (

All Issues

)} - - {groupedByIssues?.[groupTitle].length ?? 0} + + {groupedByIssues[groupTitle as keyof IIssue].length}
{type === "issue" ? (
)} - +
{link.title}
-

+

Added {timeAgo(link.created_at)}
by{" "} diff --git a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx index fad29ddaa..84b79e4df 100644 --- a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx +++ b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx @@ -100,13 +100,13 @@ export const SidebarProgressStats: React.FC = ({ > - `w-full rounded px-3 py-1 text-gray-900 ${ - selected ? " bg-theme text-white" : " hover:bg-hover-gray" + `w-full rounded px-3 py-1 text-brand-base ${ + selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2" }` } > @@ -114,8 +114,8 @@ export const SidebarProgressStats: React.FC = ({ - `w-full rounded px-3 py-1 text-gray-900 ${ - selected ? " bg-theme text-white" : " hover:bg-hover-gray" + `w-full rounded px-3 py-1 text-brand-base ${ + selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2" }` } > @@ -123,8 +123,8 @@ export const SidebarProgressStats: React.FC = ({ - `w-full rounded px-3 py-1 text-gray-900 ${ - selected ? " bg-theme text-white" : " hover:bg-hover-gray" + `w-full rounded px-3 py-1 text-brand-base ${ + selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2" }` } > @@ -166,7 +166,7 @@ export const SidebarProgressStats: React.FC = ({ -

+
= ({ }) => (
{title}
diff --git a/apps/app/components/core/theme-switch.tsx b/apps/app/components/core/theme-switch.tsx new file mode 100644 index 000000000..f93b1998e --- /dev/null +++ b/apps/app/components/core/theme-switch.tsx @@ -0,0 +1,60 @@ +import { useState, useEffect, ChangeEvent } from "react"; +import { useTheme } from "next-themes"; +import { THEMES_OBJ } from "constants/themes"; +import { CustomSelect } from "components/ui"; +import { CustomThemeModal } from "./custom-theme-modal"; + +export const ThemeSwitch = () => { + const [mounted, setMounted] = useState(false); + const [customThemeModal, setCustomThemeModal] = useState(false); + const { theme, setTheme } = useTheme(); + + // useEffect only runs on the client, so now we can safely show the UI + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( + <> + t.value === theme)?.label : "Select your theme"} + onChange={({ value, type }: { value: string; type: string }) => { + if (value === "custom") { + if (!customThemeModal) setCustomThemeModal(true); + } else { + const cssVars = [ + "--color-bg-base", + "--color-bg-surface-1", + "--color-bg-surface-2", + + "--color-border", + "--color-bg-sidebar", + "--color-accent", + + "--color-text-base", + "--color-text-secondary", + ]; + cssVars.forEach((cssVar) => document.documentElement.style.removeProperty(cssVar)); + } + document.documentElement.style.setProperty("color-scheme", type); + setTheme(value); + }} + input + width="w-full" + position="right" + > + {THEMES_OBJ.map(({ value, label, type }) => ( + + {label} + + ))} + + {/* setCustomThemeModal(false)} /> */} + + ); +}; diff --git a/apps/app/components/cycles/completed-cycles-list.tsx b/apps/app/components/cycles/completed-cycles-list.tsx index cb8bb43d7..bf1971368 100644 --- a/apps/app/components/cycles/completed-cycles-list.tsx +++ b/apps/app/components/cycles/completed-cycles-list.tsx @@ -64,7 +64,7 @@ export const CompletedCyclesList: React.FC = ({ {completedCycles ? ( completedCycles.completed_cycles.length > 0 ? (
-
+
Completed cycles are not editable.
diff --git a/apps/app/components/cycles/cycles-list.tsx b/apps/app/components/cycles/cycles-list.tsx index 28cd171d0..68592b051 100644 --- a/apps/app/components/cycles/cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list.tsx @@ -62,8 +62,8 @@ export const CyclesList: React.FC = ({
) : type === "current" ? ( showNoCurrentCycleMessage && ( -
-

No current cycle is present.

+
+

No current cycle is present.

diff --git a/apps/app/components/cycles/delete-cycle-modal.tsx b/apps/app/components/cycles/delete-cycle-modal.tsx index c43d34d68..b69537cdc 100644 --- a/apps/app/components/cycles/delete-cycle-modal.tsx +++ b/apps/app/components/cycles/delete-cycle-modal.tsx @@ -139,7 +139,7 @@ export const DeleteCycleModal: React.FC = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
@@ -153,8 +153,8 @@ export const DeleteCycleModal: React.FC = ({ leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - -
+ +
= ({ />
- + Delete Cycle
-

+

Are you sure you want to delete cycle-{" "} {data?.name}? All of the data related to the cycle will be permanently removed. This action cannot be undone. diff --git a/apps/app/components/cycles/empty-cycle.tsx b/apps/app/components/cycles/empty-cycle.tsx index dfefa4011..af2f12a11 100644 --- a/apps/app/components/cycles/empty-cycle.tsx +++ b/apps/app/components/cycles/empty-cycle.tsx @@ -37,30 +37,30 @@ export const EmptyCycle = () => { return (

-
+
- Cycle Name + Cycle Name
- - + +
-
+
-
+
- Cycle Name + Cycle Name
- - + +
-
+
@@ -68,7 +68,7 @@ export const EmptyCycle = () => {

Create New Cycle

-

+

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

diff --git a/apps/app/components/cycles/form.tsx b/apps/app/components/cycles/form.tsx index 99e55ce7a..168f4f2cd 100644 --- a/apps/app/components/cycles/form.tsx +++ b/apps/app/components/cycles/form.tsx @@ -94,7 +94,7 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat return (
-

+

{status ? "Update" : "Create"} Cycle

@@ -196,7 +196,7 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat
-
+
Cancel = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
@@ -164,7 +164,7 @@ export const CreateUpdateCycleModal: React.FC = ({ leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - + = ({ {({ open }) => ( <> - +
{cycles?.find((c) => c.id === value)?.name ?? "Cycles"}
@@ -75,7 +75,7 @@ export const CycleSelect: React.FC = ({ leaveTo="opacity-0" >
{options ? ( @@ -93,7 +93,7 @@ export const CycleSelect: React.FC = ({ : "" } ${ active ? "bg-indigo-50" : "" - } relative cursor-pointer select-none p-2 text-gray-900` + } relative cursor-pointer select-none p-2 text-brand-base` } value={option.value} > @@ -103,14 +103,14 @@ export const CycleSelect: React.FC = ({ )) ) : ( -

No options

+

No options

) ) : ( -

Loading...

+

Loading...

)}
-
- +
+ = ({ isOpen, handleClose }) => filteredOptions.map((option: ICycle) => (
diff --git a/apps/app/components/cycles/transfer-issues.tsx b/apps/app/components/cycles/transfer-issues.tsx index 979e87b93..59a10a4d8 100644 --- a/apps/app/components/cycles/transfer-issues.tsx +++ b/apps/app/components/cycles/transfer-issues.tsx @@ -38,7 +38,7 @@ export const TransferIssues: React.FC = ({ handleClick }) => { : 0; return (
-
+
Completed cycles are not editable.
diff --git a/apps/app/components/emoji-icon-picker/index.tsx b/apps/app/components/emoji-icon-picker/index.tsx index 6c3d3842b..6cb3b84f9 100644 --- a/apps/app/components/emoji-icon-picker/index.tsx +++ b/apps/app/components/emoji-icon-picker/index.tsx @@ -55,7 +55,7 @@ const EmojiIconPicker: React.FC = ({ return ( setIsOpen((prev) => !prev)} > {label} @@ -69,8 +69,8 @@ const EmojiIconPicker: React.FC = ({ leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - -
+ +
{tabOptions.map((tab) => ( @@ -100,7 +100,7 @@ const EmojiIconPicker: React.FC = ({ {recentEmojis.map((emoji) => ( +
+ + )} + /> +
+
+
+ ); +}; diff --git a/apps/app/components/integration/jira/import-users.tsx b/apps/app/components/integration/jira/import-users.tsx new file mode 100644 index 000000000..fa48bc772 --- /dev/null +++ b/apps/app/components/integration/jira/import-users.tsx @@ -0,0 +1,145 @@ +import { FC } from "react"; + +// next +import { useRouter } from "next/router"; + +// react-hook-form +import { useFormContext, useFieldArray, Controller } from "react-hook-form"; + +// hooks +import useWorkspaceMembers from "hooks/use-workspace-members"; + +// components +import { ToggleSwitch, Input, CustomSelect, CustomSearchSelect, Avatar } from "components/ui"; + +import { IJiraImporterForm } from "types"; + +export const JiraImportUsers: FC = () => { + const { + control, + watch, + register, + formState: { errors }, + } = useFormContext(); + + const { fields } = useFieldArray({ + control, + name: "data.users", + }); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { workspaceMembers: members } = useWorkspaceMembers(workspaceSlug?.toString()); + + const options = + members?.map((member) => ({ + value: member.member.email, + query: + (member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email) + + " " + + member.member.last_name ?? "", + content: ( +
+ + {member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + " (" + member.member.email + ")" + : member.member.email} +
+ ), + })) ?? []; + + return ( +
+
+
+

Users

+

Update, invite or choose not to invite assignee

+
+
+ ( + + )} + /> +
+
+ + {watch("data.invite_users") && ( +
+
+
Name
+
Import as
+
+ +
+ {fields.map((user, index) => ( +
+
+

{user.username}

+
+
+ ( + + {Boolean(value) ? value : ("Ignore" as any)} + + } + > + Invite by email + Map to existing + Do not import + + )} + /> +
+
+ {watch(`data.users.${index}.import`) === "invite" && ( + + )} + {watch(`data.users.${index}.import`) === "map" && ( + ( + + )} + /> + )} +
+
+ ))} +
+
+ )} +
+ ); +}; diff --git a/apps/app/components/integration/jira/index.ts b/apps/app/components/integration/jira/index.ts index 1efe34c51..321e4f313 100644 --- a/apps/app/components/integration/jira/index.ts +++ b/apps/app/components/integration/jira/index.ts @@ -1 +1,39 @@ export * from "./root"; +export * from "./give-details"; +export * from "./jira-project-detail"; +export * from "./import-users"; +export * from "./confirm-import"; + +import { IJiraImporterForm } from "types"; + +export type TJiraIntegrationSteps = + | "import-configure" + | "display-import-data" + | "select-import-data" + | "import-users" + | "import-confirmation"; + +export interface IJiraIntegrationData { + state: TJiraIntegrationSteps; +} + +export const jiraFormDefaultValues: IJiraImporterForm = { + metadata: { + cloud_hostname: "", + api_token: "", + project_key: "", + email: "", + }, + config: { + epics_to_modules: false, + }, + data: { + users: [], + invite_users: true, + total_issues: 0, + total_labels: 0, + total_modules: 0, + total_states: 0, + }, + project_id: "", +}; diff --git a/apps/app/components/integration/jira/jira-project-detail.tsx b/apps/app/components/integration/jira/jira-project-detail.tsx new file mode 100644 index 000000000..48220a8c1 --- /dev/null +++ b/apps/app/components/integration/jira/jira-project-detail.tsx @@ -0,0 +1,168 @@ +import React, { useEffect } from "react"; + +// next +import { useRouter } from "next/router"; + +// swr +import useSWR from "swr"; + +// react hook form +import { useFormContext, Controller } from "react-hook-form"; + +// services +import jiraImporterService from "services/integration/jira.service"; + +// fetch keys +import { JIRA_IMPORTER_DETAIL } from "constants/fetch-keys"; + +import { IJiraImporterForm, IJiraMetadata } from "types"; + +// components +import { Spinner, ToggleSwitch } from "components/ui"; + +import type { IJiraIntegrationData, TJiraIntegrationSteps } from "./"; + +type Props = { + setCurrentStep: React.Dispatch>; + setDisableTopBarAfter: React.Dispatch>; +}; + +export const JiraProjectDetail: React.FC = (props) => { + const { setCurrentStep, setDisableTopBarAfter } = props; + + const { + watch, + setValue, + control, + formState: { errors }, + } = useFormContext(); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const params: IJiraMetadata = { + api_token: watch("metadata.api_token"), + project_key: watch("metadata.project_key"), + email: watch("metadata.email"), + cloud_hostname: watch("metadata.cloud_hostname"), + }; + + const { data: projectInfo, error } = useSWR( + workspaceSlug && + !errors.metadata?.api_token && + !errors.metadata?.project_key && + !errors.metadata?.email && + !errors.metadata?.cloud_hostname + ? JIRA_IMPORTER_DETAIL(workspaceSlug.toString(), params) + : null, + workspaceSlug && + !errors.metadata?.api_token && + !errors.metadata?.project_key && + !errors.metadata?.email && + !errors.metadata?.cloud_hostname + ? () => jiraImporterService.getJiraProjectInfo(workspaceSlug.toString(), params) + : null + ); + + useEffect(() => { + if (!projectInfo) return; + + setValue("data.total_issues", projectInfo.issues); + setValue("data.total_labels", projectInfo.labels); + setValue( + "data.users", + projectInfo.users?.map((user) => ({ + email: user.emailAddress, + import: false, + username: user.displayName, + })) + ); + setValue("data.total_states", projectInfo.states); + setValue("data.total_modules", projectInfo.modules); + }, [projectInfo, setValue]); + + useEffect(() => { + if (error) setDisableTopBarAfter("display-import-data"); + else setDisableTopBarAfter(null); + }, [error, setDisableTopBarAfter]); + + useEffect(() => { + if (!projectInfo && !error) setDisableTopBarAfter("display-import-data"); + else if (!error) setDisableTopBarAfter(null); + }, [projectInfo, error, setDisableTopBarAfter]); + + if (!projectInfo && !error) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+

+ Something went wrong. Please{" "} + {" "} + and check your Jira project details. +

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

Import Data

+

Import Completed. We have found:

+
+
+
+

{projectInfo?.issues}

+

Issues

+
+
+

{projectInfo?.states}

+

States

+
+
+

{projectInfo?.modules}

+

Modules

+
+
+

{projectInfo?.labels}

+

Labels

+
+
+

{projectInfo?.users?.length}

+

Users

+
+
+
+ +
+
+

Import Epics

+

Import epics as modules

+
+
+ ( + + )} + /> +
+
+
+ ); +}; diff --git a/apps/app/components/integration/jira/root.tsx b/apps/app/components/integration/jira/root.tsx index 15a89c3e4..b9bd732c0 100644 --- a/apps/app/components/integration/jira/root.tsx +++ b/apps/app/components/integration/jira/root.tsx @@ -1 +1,224 @@ -export const JiraImporterRoot = () => <>; +import React, { useState } from "react"; + +// next +import Link from "next/link"; +import Image from "next/image"; +import { useRouter } from "next/router"; + +// swr +import { mutate } from "swr"; + +// react hook form +import { FormProvider, useForm } from "react-hook-form"; + +// icons +import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline"; +import { CogIcon, CloudUploadIcon, UsersIcon, CheckIcon } from "components/icons"; + +// services +import jiraImporterService from "services/integration/jira.service"; + +// fetch keys +import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; + +// components +import { PrimaryButton, SecondaryButton } from "components/ui"; +import { + JiraGetImportDetail, + JiraProjectDetail, + JiraImportUsers, + JiraConfirmImport, + jiraFormDefaultValues, + TJiraIntegrationSteps, + IJiraIntegrationData, +} from "./"; + +import JiraLogo from "public/services/jira.png"; + +import { IJiraImporterForm } from "types"; + +const integrationWorkflowData: Array<{ + title: string; + key: TJiraIntegrationSteps; + icon: React.FC>; +}> = [ + { + title: "Configure", + key: "import-configure", + icon: CogIcon, + }, + { + title: "Import Data", + key: "display-import-data", + icon: ListBulletIcon, + }, + { + title: "Users", + key: "import-users", + icon: UsersIcon, + }, + { + title: "Confirm", + key: "import-confirmation", + icon: CheckIcon, + }, +]; + +export const JiraImporterRoot = () => { + const [currentStep, setCurrentStep] = useState({ + state: "import-configure", + }); + const [disableTopBarAfter, setDisableTopBarAfter] = useState(null); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const methods = useForm({ + defaultValues: jiraFormDefaultValues, + mode: "all", + reValidateMode: "onChange", + }); + + const isValid = methods.formState.isValid; + + const onSubmit = async (data: IJiraImporterForm) => { + if (!workspaceSlug) return; + + await jiraImporterService + .createJiraImporter(workspaceSlug.toString(), data) + .then(() => { + mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString())); + router.push(`/${workspaceSlug}/settings/import-export`); + }) + .catch((err) => { + console.log(err); + }); + }; + + const activeIntegrationState = () => { + const currentElementIndex = integrationWorkflowData.findIndex( + (i) => i?.key === currentStep?.state + ); + + return currentElementIndex; + }; + + return ( +
+ +
+
+ +
+
Cancel import & go back
+
+ + +
+
+
+ jira logo +
+
+ {integrationWorkflowData.map((integration, index) => ( + + + {index < integrationWorkflowData.length - 1 && ( +
+ {" "} +
+ )} +
+ ))} +
+
+ +
+ + +
+ {currentStep.state === "import-configure" && } + {currentStep.state === "display-import-data" && ( + + )} + {currentStep?.state === "import-users" && } + {currentStep?.state === "import-confirmation" && } +
+ +
+ {currentStep?.state !== "import-configure" && ( + { + const currentElementIndex = integrationWorkflowData.findIndex( + (i) => i?.key === currentStep?.state + ); + setCurrentStep({ + state: integrationWorkflowData[currentElementIndex - 1]?.key, + }); + }} + > + Back + + )} + { + const currentElementIndex = integrationWorkflowData.findIndex( + (i) => i?.key === currentStep?.state + ); + + if (currentElementIndex === integrationWorkflowData.length - 1) { + methods.handleSubmit(onSubmit)(); + } else { + setCurrentStep({ + state: integrationWorkflowData[currentElementIndex + 1]?.key, + }); + } + }} + > + {currentStep?.state === "import-confirmation" ? "Confirm & Import" : "Next"} + +
+ +
+
+
+
+ ); +}; diff --git a/apps/app/components/integration/single-integration-card.tsx b/apps/app/components/integration/single-integration-card.tsx index a2fe1061d..d6ac13bc8 100644 --- a/apps/app/components/integration/single-integration-card.tsx +++ b/apps/app/components/integration/single-integration-card.tsx @@ -99,7 +99,7 @@ export const SingleIntegrationCard: React.FC = ({ integration }) => { ); return ( -
+
, + icon:
{issueActivities.length > 1 && activityItemIdx !== issueActivities.length - 1 ? (
-
+
{activityItem.field ? ( activityDetails[activityItem.field as keyof typeof activityDetails] ?.icon @@ -332,7 +336,7 @@ export const IssueActivitySection: React.FC = () => {
-
+
{activityItem.actor_detail.first_name} {activityItem.actor_detail.is_bot @@ -340,7 +344,7 @@ export const IssueActivitySection: React.FC = () => { : " " + activityItem.actor_detail.last_name} {action} - {value} + {value} {timeAgo(activityItem.created_at)} @@ -353,12 +357,13 @@ export const IssueActivitySection: React.FC = () => { ); } else if ("comment_json" in activityItem) return ( - +
+ +
); })} diff --git a/apps/app/components/issues/attachment-upload.tsx b/apps/app/components/issues/attachment-upload.tsx index 6819807ba..7ad8d1d35 100644 --- a/apps/app/components/issues/attachment-upload.tsx +++ b/apps/app/components/issues/attachment-upload.tsx @@ -96,9 +96,9 @@ export const IssueAttachmentUpload = () => { ) : fileError ? (

{fileError}

) : isLoading ? ( -

Uploading....

+

Uploading...

) : ( -

Drag and drop/Click to add

+

Click or drag a file here

)}
diff --git a/apps/app/components/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx index 307b4b0b4..8423b3003 100644 --- a/apps/app/components/issues/comment/add-comment.tsx +++ b/apps/app/components/issues/comment/add-comment.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React from "react"; import { useRouter } from "next/router"; import dynamic from "next/dynamic"; @@ -9,10 +9,10 @@ import { mutate } from "swr"; import { useForm, Controller } from "react-hook-form"; // services import issuesServices from "services/issues.service"; +// hooks +import useToast from "hooks/use-toast"; // ui -import { Loader } from "components/ui"; -// helpers -import { debounce } from "helpers/common.helper"; +import { Loader, SecondaryButton } from "components/ui"; // types import type { IIssueComment } from "types"; // fetch-keys @@ -28,8 +28,8 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor }); const defaultValues: Partial = { - comment_html: "", comment_json: "", + comment_html: "", }; export const AddComment: React.FC = () => { @@ -42,9 +42,10 @@ export const AddComment: React.FC = () => { } = useForm({ defaultValues }); const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; + const { setToastAlert } = useToast(); + const onSubmit = async (formData: IIssueComment) => { if ( !workspaceSlug || @@ -61,53 +62,35 @@ export const AddComment: React.FC = () => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); reset(defaultValues); }) - .catch((error) => { - console.error(error); - }); + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Comment could not be posted. Please try again.", + }) + ); }; - const updateDescription = useMemo( - () => - debounce((key: any, val: any) => { - setValue(key, val); - }, 1000), - [setValue] - ); - - const updateDescriptionHTML = useMemo( - () => - debounce((key: any, val: any) => { - setValue(key, val); - }, 1000), - [setValue] - ); - return ( -
+
( { - setValue("comment_json", jsonValue); - setValue("comment_html", htmlValue); - }} + onJSONChange={(jsonValue) => setValue("comment_json", jsonValue)} + onHTMLChange={(htmlValue) => setValue("comment_html", htmlValue)} placeholder="Enter your comment..." /> )} /> - +
diff --git a/apps/app/components/issues/comment/comment-card.tsx b/apps/app/components/issues/comment/comment-card.tsx index 9cab65531..4691ad705 100644 --- a/apps/app/components/issues/comment/comment-card.tsx +++ b/apps/app/components/issues/comment/comment-card.tsx @@ -67,7 +67,7 @@ export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentD
)} - +
@@ -77,7 +77,7 @@ export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentD {comment.actor_detail.first_name} {comment.actor_detail.is_bot ? "Bot" : " " + comment.actor_detail.last_name}
-

Commented {timeAgo(comment.created_at)}

+

Commented {timeAgo(comment.created_at)}

{isEditing ? ( @@ -117,7 +117,7 @@ export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentD editable={false} onBlur={() => ({})} noBorder - customClassName="text-xs bg-gray-100" + customClassName="text-xs bg-brand-surface-1" /> )}
diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx index e161e52a1..4fb5f92ea 100644 --- a/apps/app/components/issues/delete-issue-modal.tsx +++ b/apps/app/components/issues/delete-issue-modal.tsx @@ -88,7 +88,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
@@ -102,7 +102,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - +
@@ -116,7 +116,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data })
-

+

Are you sure you want to delete issue{" "} {data?.project_detail.identifier}-{data?.sequence_id} diff --git a/apps/app/components/issues/description-form.tsx b/apps/app/components/issues/description-form.tsx index 95962b929..b568fb016 100644 --- a/apps/app/components/issues/description-form.tsx +++ b/apps/app/components/issues/description-form.tsx @@ -115,7 +115,7 @@ export const IssueDescriptionForm: FC = ({ issue, handleFormS role="textbox" /> {characterLimit && ( -

+
255 ? "text-red-500" : "" @@ -158,7 +158,7 @@ export const IssueDescriptionForm: FC = ({ issue, handleFormS )} />
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index 257a6a4e1..8556c2551 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -216,12 +216,12 @@ export const IssueForm: FC = ({ /> )} /> -

+

{status ? "Update" : "Create"} Issue

{watch("parent") && watch("parent") !== "" ? ( -
+
= ({ /> {mostSimilarIssue && (
-

+

@@ -283,7 +283,7 @@ export const IssueForm: FC = ({

-
+
setCreateMore((prevData) => !prevData)} @@ -453,7 +453,7 @@ export const IssueForm: FC = ({
diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index 47fe6e871..1b74664b1 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -219,7 +219,7 @@ export const CreateUpdateIssueModal: React.FC = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
@@ -233,7 +233,7 @@ export const CreateUpdateIssueModal: React.FC = ({ leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - + = ({ issue, properties, projectId const isNotAllowed = false; return ( -
+
@@ -97,7 +97,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId )} - + {truncateText(issue.name, 50)} @@ -127,7 +127,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId /> )} {properties.sub_issue_count && ( -
+
{issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
)} @@ -136,7 +136,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId {issue.label_details.map((label) => ( = ({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - + {multiple ? ( <> ({})} multiple>
  • {query === "" && ( -

    +

    {title}

    )} @@ -182,7 +182,7 @@ export const ParentIssuesListModal: React.FC = ({ value={issue.id} className={({ active }) => `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${ - active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + active ? "bg-gray-900 bg-opacity-5 text-brand-base" : "" }` } onClick={() => handleClose()} @@ -194,7 +194,7 @@ export const ParentIssuesListModal: React.FC = ({ backgroundColor: issue.state_detail.color, }} /> - + {issue.project_detail?.identifier}-{issue.sequence_id} {" "} {issue.name} @@ -206,9 +206,9 @@ export const ParentIssuesListModal: React.FC = ({ ) : (
    -

    +

    No issues found. Create a new issue with{" "} -
    C
    . +
    C
    .

    )} diff --git a/apps/app/components/issues/select/assignee.tsx b/apps/app/components/issues/select/assignee.tsx index d47d4b178..5c1d7c1b4 100644 --- a/apps/app/components/issues/select/assignee.tsx +++ b/apps/app/components/issues/select/assignee.tsx @@ -54,16 +54,15 @@ export const IssueAssigneeSelect: React.FC = ({ projectId, value = [], on onChange={onChange} options={options} label={ -
    +
    {value && value.length > 0 && Array.isArray(value) ? (
    - - {value.length} Assignees +
    ) : (
    - - Assignee + + Assignee
    )}
    diff --git a/apps/app/components/issues/select/date.tsx b/apps/app/components/issues/select/date.tsx index 438291a2d..3bb8c1bed 100644 --- a/apps/app/components/issues/select/date.tsx +++ b/apps/app/components/issues/select/date.tsx @@ -18,11 +18,11 @@ export const IssueDateSelect: React.FC = ({ value, onChange }) => ( <> - `flex cursor-pointer items-center rounded-md border text-xs shadow-sm duration-200 + `flex cursor-pointer items-center rounded-md border border-brand-base text-xs shadow-sm duration-200 ${ open - ? "border-theme bg-theme/5 outline-none ring-1 ring-theme " - : "hover:bg-theme/5 " + ? "border-brand-accent bg-brand-accent/5 outline-none ring-1 ring-brand-accent " + : "hover:bg-brand-accent/5 " }` } > diff --git a/apps/app/components/issues/select/label.tsx b/apps/app/components/issues/select/label.tsx index c05c8a8c8..f5b023280 100644 --- a/apps/app/components/issues/select/label.tsx +++ b/apps/app/components/issues/select/label.tsx @@ -60,11 +60,11 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, <> - `flex cursor-pointer items-center rounded-md border text-xs shadow-sm duration-200 + `flex cursor-pointer items-center rounded-md border border-brand-base text-xs shadow-sm duration-200 ${ open - ? "border-theme bg-theme/5 outline-none ring-1 ring-theme " - : "hover:bg-theme/5 " + ? "border-brand-accent bg-brand-accent/5 outline-none ring-1 ring-brand-accent " + : "hover:bg-brand-accent/5 " }` } > @@ -73,14 +73,13 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, issueLabels?.find((l) => l.id === v)?.color) ?? []} length={3} - showLength + showLength={true} /> - {value.length} Labels ) : ( - - Label + + Label )} @@ -97,12 +96,12 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, > -
    - +
    + setQuery(event.target.value)} placeholder="Search for label..." displayValue={(assigned: any) => assigned?.name} @@ -121,7 +120,7 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, key={label.id} className={({ active }) => `${ - active ? "bg-gray-200" : "" + active ? "bg-brand-surface-2" : "" } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600` } value={label.id} @@ -151,8 +150,8 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, ); } else return ( -
    -
    +
    +
    {label.name}
    @@ -161,7 +160,7 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, key={child.id} className={({ active }) => `${ - active ? "bg-gray-200" : "" + active ? "bg-brand-surface-2" : "" } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600` } value={child.id} @@ -193,14 +192,14 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, ); }) ) : ( -

    No labels found

    +

    No labels found

    ) ) : ( -

    Loading...

    +

    Loading...

    )}
    -
    +
    = ({ issueDetail?.parent_detail ? ( ) : ( -
    +
    No parent selected
    ) @@ -344,7 +344,7 @@ export const IssueDetailsSidebar: React.FC = ({ userAuth={memberRole} />
    -
    +

    Due date

    @@ -382,7 +382,7 @@ export const IssueDetailsSidebar: React.FC = ({
    -
    +

    Label

    @@ -395,7 +395,7 @@ export const IssueDetailsSidebar: React.FC = ({ return ( { const updatedLabels = watchIssue("labels_list")?.filter( (l) => l !== labelId @@ -435,8 +435,8 @@ export const IssueDetailsSidebar: React.FC = ({ className={`flex ${ isNotAllowed ? "cursor-not-allowed" - : "cursor-pointer hover:bg-gray-100" - } items-center gap-2 rounded-2xl border px-2 py-0.5 text-xs`} + : "cursor-pointer hover:bg-brand-surface-1" + } items-center gap-2 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary`} > Select Label @@ -448,7 +448,7 @@ export const IssueDetailsSidebar: React.FC = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > - +
    {issueLabels ? ( issueLabels.length > 0 ? ( @@ -463,9 +463,11 @@ export const IssueDetailsSidebar: React.FC = ({ - `${active || selected ? "bg-indigo-50" : ""} ${ + `${ + active || selected ? "bg-brand-surface-1" : "" + } ${ selected ? "font-medium" : "" - } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-brand-base` } value={label.id} > @@ -483,8 +485,8 @@ export const IssueDetailsSidebar: React.FC = ({ ); } else return ( -
    -
    +
    +
    {" "} {label.name}
    @@ -495,7 +497,7 @@ export const IssueDetailsSidebar: React.FC = ({ className={({ active, selected }) => `${active || selected ? "bg-indigo-50" : ""} ${ selected ? "font-medium" : "" - } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-brand-base` } value={child.id} > @@ -530,8 +532,10 @@ export const IssueDetailsSidebar: React.FC = ({ )} diff --git a/apps/app/components/issues/view-select/assignee.tsx b/apps/app/components/issues/view-select/assignee.tsx index 1429555d9..188543185 100644 --- a/apps/app/components/issues/view-select/assignee.tsx +++ b/apps/app/components/issues/view-select/assignee.tsx @@ -103,17 +103,17 @@ export const ViewAssigneeSelect: React.FC = ({
    {issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? ( -
    +
    - {issue.assignees.length} Assignees + {issue.assignees.length} Assignees
    ) : (
    - - Assignee + + Assignee
    )}
    diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index 11cd4e698..eb20f7040 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -52,19 +52,19 @@ export const ViewPrioritySelect: React.FC = ({ customButton={