diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75ccb884c..6baa0bb07 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,6 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla - Python version 3.8+ - Postgres version v14 - Redis version v6.2.7 -- pnpm version 7.22.0 ### Setup the project diff --git a/README.md b/README.md index 7e9422f18..1e600df5e 100644 --- a/README.md +++ b/README.md @@ -61,14 +61,6 @@ chmod +x setup.sh > If running in a cloud env replace localhost with public facing IP address of the VM -- Export Environment Variables - -```bash -set -a -source .env -set +a -``` - - Run Docker compose up ```bash diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 18ee19e7b..ed369f20a 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -77,6 +77,13 @@ class ProjectSerializer(BaseSerializer): raise serializers.ValidationError(detail="Project Identifier is already taken") +class ProjectLiteSerializer(BaseSerializer): + class Meta: + model = Project + fields = ["id", "identifier", "name"] + read_only_fields = fields + + class ProjectDetailSerializer(BaseSerializer): workspace = WorkSpaceSerializer(read_only=True) default_assignee = UserLiteSerializer(read_only=True) @@ -94,7 +101,7 @@ class ProjectDetailSerializer(BaseSerializer): class ProjectMemberSerializer(BaseSerializer): workspace = WorkSpaceSerializer(read_only=True) - project = ProjectSerializer(read_only=True) + project = ProjectLiteSerializer(read_only=True) member = UserLiteSerializer(read_only=True) class Meta: @@ -127,10 +134,3 @@ class ProjectFavoriteSerializer(BaseSerializer): "workspace", "user", ] - - -class ProjectLiteSerializer(BaseSerializer): - class Meta: - model = Project - fields = ["id", "identifier", "name"] - read_only_fields = fields diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 936fd73ab..806ebcd6f 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -295,7 +295,6 @@ urlpatterns = [ { "delete": "destroy", "get": "retrieve", - "get": "retrieve", } ), name="workspace", diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 36b3411fc..bfefc91ba 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -256,7 +256,7 @@ class IssueViewSet(BaseViewSet): return Response(issues, 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/project.py b/apiserver/plane/api/views/project.py index 68a34ab48..822dc78b5 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -259,7 +259,7 @@ class ProjectViewSet(BaseViewSet): group="backlog", description="Default state for managing all Inbox Issues", project_id=pk, - color="#ff7700" + color="#ff7700", ) return Response(serializer.data, status=status.HTTP_200_OK) @@ -550,45 +550,47 @@ class AddMemberToProjectEndpoint(BaseAPIView): def post(self, request, slug, project_id): try: - member_id = request.data.get("member_id", False) - role = request.data.get("role", False) + members = request.data.get("members", []) - if not member_id or not role: + # get the project + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + if not len(members): return Response( - {"error": "Member ID and role is required"}, + {"error": "Atleast one member is required"}, status=status.HTTP_400_BAD_REQUEST, ) - # Check if the user is a member in the workspace - if not WorkspaceMember.objects.filter( - workspace__slug=slug, member_id=member_id - ).exists(): - # TODO: Update this error message - nk - return Response( - { - "error": "User is not a member of the workspace. Invite the user to the workspace to add him to project" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Check if the user is already member of project - if ProjectMember.objects.filter( - project=project_id, member_id=member_id - ).exists(): - return Response( - {"error": "User is already a member of the project"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Add the user to project - project_member = ProjectMember.objects.create( - project_id=project_id, member_id=member_id, role=role + project_members = ProjectMember.objects.bulk_create( + [ + ProjectMember( + member_id=member.get("member_id"), + role=member.get("role", 10), + project_id=project_id, + workspace_id=project.workspace_id, + ) + for member in members + ], + batch_size=10, + ignore_conflicts=True, ) - serializer = ProjectMemberSerializer(project_member) + serializer = ProjectMemberSerializer(project_members, many=True) return Response(serializer.data, status=status.HTTP_201_CREATED) - + except KeyError: + return Response( + {"error": "Incorrect data sent"}, status=status.HTTP_400_BAD_REQUEST + ) + except Project.DoesNotExist: + return Response( + {"error": "Project does not exist"}, status=status.HTTP_400_BAD_REQUEST + ) + except IntegrityError: + return Response( + {"error": "User not member of the workspace"}, + status=status.HTTP_400_BAD_REQUEST, + ) except Exception as e: capture_exception(e) return Response( diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 26c82d54c..4c136ed8c 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -3,6 +3,7 @@ import jwt from datetime import date, datetime from dateutil.relativedelta import relativedelta from uuid import uuid4 + # Django imports from django.db import IntegrityError from django.db.models import Prefetch @@ -21,6 +22,7 @@ from django.db.models import ( ) from django.db.models.functions import ExtractWeek, Cast, ExtractDay from django.db.models.fields import DateField +from django.contrib.auth.hashers import make_password # Third party modules from rest_framework import status @@ -93,14 +95,33 @@ class WorkSpaceViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - return self.filter_queryset( - super().get_queryset().select_related("owner") - ).order_by("name").filter(workspace_member__member=self.request.user).annotate(total_members=member_count).annotate(total_issues=issue_count) + return ( + self.filter_queryset(super().get_queryset().select_related("owner")) + .order_by("name") + .filter(workspace_member__member=self.request.user) + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + ) def create(self, request): try: serializer = WorkSpaceSerializer(data=request.data) + slug = request.data.get("slug", False) + name = request.data.get("name", False) + + if not name or not slug: + return Response( + {"error": "Both name and slug are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if len(name) > 80 or len(slug) > 48: + return Response( + {"error": "The maximum length for name is 80 and for slug is 48"}, + status=status.HTTP_400_BAD_REQUEST, + ) + if serializer.is_valid(): serializer.save(owner=request.user) # Create Workspace member @@ -160,14 +181,20 @@ class UserWorkSpacesEndpoint(BaseAPIView): ) workspace = ( - Workspace.objects.prefetch_related( - Prefetch("workspace_member", queryset=WorkspaceMember.objects.all()) + ( + Workspace.objects.prefetch_related( + Prefetch( + "workspace_member", queryset=WorkspaceMember.objects.all() + ) + ) + .filter( + workspace_member__member=request.user, + ) + .select_related("owner") ) - .filter( - workspace_member__member=request.user, - ) - .select_related("owner") - ).annotate(total_members=member_count).annotate(total_issues=issue_count) + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + ) serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) return Response(serializer.data, status=status.HTTP_200_OK) @@ -216,9 +243,20 @@ class InviteWorkspaceEndpoint(BaseAPIView): ) # check for role level - requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user) - if len([email for email in emails if int(email.get("role", 10)) > requesting_user.role]): - return Response({"error": "You cannot invite a user with higher role"}, status=status.HTTP_400_BAD_REQUEST) + requesting_user = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + if len( + [ + email + for email in emails + if int(email.get("role", 10)) > requesting_user.role + ] + ): + return Response( + {"error": "You cannot invite a user with higher role"}, + status=status.HTTP_400_BAD_REQUEST, + ) workspace = Workspace.objects.get(slug=slug) @@ -276,14 +314,18 @@ class InviteWorkspaceEndpoint(BaseAPIView): # create the user if signup is disabled if settings.DOCKERIZED and not settings.ENABLE_SIGNUP: - _ = User.objects.bulk_create([ - User( - email=email.get("email"), - password=str(uuid4().hex), - is_password_autoset=True - ) - for email in emails - ], batch_size=100) + _ = User.objects.bulk_create( + [ + User( + username=str(uuid4().hex), + email=invitation.email, + password=make_password(uuid4().hex), + is_password_autoset=True, + ) + for invitation in workspace_invitations + ], + batch_size=100, + ) for invitation in workspace_invitations: workspace_invitation.delay( @@ -400,6 +442,30 @@ class WorkspaceInvitationsViewset(BaseViewSet): .select_related("workspace", "workspace__owner", "created_by") ) + def destroy(self, request, slug, pk): + try: + workspace_member_invite = WorkspaceMemberInvite.objects.get( + pk=pk, workspace__slug=slug + ) + # delete the user if signup is disabled + if settings.DOCKERIZED and not settings.ENABLE_SIGNUP: + user = User.objects.filter(email=workspace_member_invite.email).first() + if user is not None: + user.delete() + workspace_member_invite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except WorkspaceMemberInvite.DoesNotExist: + return Response( + {"error": "Workspace member invite 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 UserWorkspaceInvitationsEndpoint(BaseViewSet): serializer_class = WorkSpaceMemberInviteSerializer @@ -865,7 +931,9 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView): ) state_distribution = ( - Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user]) + Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] + ) .annotate(state_group=F("state__group")) .values("state_group") .annotate(state_count=Count("state_group")) diff --git a/apiserver/plane/db/migrations/0035_auto_20230704_2225.py b/apiserver/plane/db/migrations/0035_auto_20230704_2225.py new file mode 100644 index 000000000..dec6265e6 --- /dev/null +++ b/apiserver/plane/db/migrations/0035_auto_20230704_2225.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.19 on 2023-07-04 16:55 + +from django.db import migrations, models + + +def update_company_organization_size(apps, schema_editor): + Model = apps.get_model("db", "Workspace") + updated_size = [] + for obj in Model.objects.all(): + obj.organization_size = str(obj.company_size) + updated_size.append(obj) + + Model.objects.bulk_update(updated_size, ["organization_size"], batch_size=100) + + +class Migration(migrations.Migration): + dependencies = [ + ("db", "0034_auto_20230628_1046"), + ] + + operations = [ + migrations.AddField( + model_name="workspace", + name="organization_size", + field=models.CharField(default="2-10", max_length=20), + ), + migrations.RunPython(update_company_organization_size), + migrations.AlterField( + model_name="workspace", + name="name", + field=models.CharField(max_length=80, verbose_name="Workspace Name"), + ), + migrations.AlterField( + model_name="workspace", + name="slug", + field=models.SlugField(max_length=48, unique=True), + ), + migrations.RemoveField( + model_name="workspace", + name="company_size", + ), + ] diff --git a/apiserver/plane/db/migrations/0036_alter_workspace_organization_size.py b/apiserver/plane/db/migrations/0036_alter_workspace_organization_size.py new file mode 100644 index 000000000..0b182f50b --- /dev/null +++ b/apiserver/plane/db/migrations/0036_alter_workspace_organization_size.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.19 on 2023-07-05 07:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0035_auto_20230704_2225'), + ] + + operations = [ + migrations.AlterField( + model_name='workspace', + name='organization_size', + field=models.CharField(max_length=20), + ), + ] diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index b00d53013..9b9fbb68c 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -15,15 +15,15 @@ ROLE_CHOICES = ( class Workspace(BaseModel): - name = models.CharField(max_length=255, verbose_name="Workspace Name") + name = models.CharField(max_length=80, verbose_name="Workspace Name") logo = models.URLField(verbose_name="Logo", blank=True, null=True) owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="owner_workspace", ) - slug = models.SlugField(max_length=100, db_index=True, unique=True) - company_size = models.PositiveIntegerField(default=10) + slug = models.SlugField(max_length=48, db_index=True, unique=True) + organization_size = models.CharField(max_length=20) def __str__(self): """Return name of the Workspace""" diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 1b862c013..e6f5f8e39 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -63,6 +63,7 @@ if os.environ.get("SENTRY_DSN", False): send_default_pii=True, environment="local", traces_sample_rate=0.7, + profiles_sample_rate=1.0, ) REDIS_HOST = "localhost" diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index 29b75fc8b..7e76404f6 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -13,9 +13,7 @@ from sentry_sdk.integrations.redis import RedisIntegration from .common import * # noqa # Database -DEBUG = int(os.environ.get( - "DEBUG", 0 -)) == 1 +DEBUG = int(os.environ.get("DEBUG", 0)) == 1 DATABASES = { "default": { @@ -84,11 +82,12 @@ if bool(os.environ.get("SENTRY_DSN", False)): traces_sample_rate=1, send_default_pii=True, environment="production", + profiles_sample_rate=1.0, ) if DOCKERIZED and USE_MINIO: INSTALLED_APPS += ("storages",) - DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" + STORAGES = {"default": {"BACKEND": "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. @@ -96,7 +95,9 @@ if DOCKERIZED and USE_MINIO: # 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") + 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 diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index 11ff7a372..5a43e266e 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -66,6 +66,7 @@ sentry_sdk.init( traces_sample_rate=1, send_default_pii=True, environment="staging", + profiles_sample_rate=1.0, ) # The AWS region to connect to. diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index a2244ffe0..2b83ef8cf 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -3,11 +3,10 @@ """ # from django.contrib import admin -from django.urls import path +from django.urls import path, include, re_path from django.views.generic import TemplateView from django.conf import settings -from django.conf.urls import include, url, static # from django.conf.urls.static import static @@ -18,11 +17,10 @@ urlpatterns = [ path("", include("plane.web.urls")), ] -urlpatterns = urlpatterns + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: import debug_toolbar urlpatterns = [ - url(r"^__debug__/", include(debug_toolbar.urls)), + re_path(r"^__debug__/", include(debug_toolbar.urls)), ] + urlpatterns diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 74acb2044..6a9e8b8e8 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -166,16 +166,16 @@ def filter_target_date(params, filter, method): for query in target_dates: target_date_query = query.split(";") if len(target_date_query) == 2 and "after" in target_date_query: - filter["target_date__gte"] = target_date_query[0] + filter["target_date__gt"] = target_date_query[0] else: - filter["target_date__lte"] = target_date_query[0] + filter["target_date__lt"] = target_date_query[0] else: if params.get("target_date", None) and len(params.get("target_date")): for query in params.get("target_date"): if query.get("timeline", "after") == "after": - filter["target_date__gte"] = query.get("datetime") + filter["target_date__gt"] = query.get("datetime") else: - filter["target_date__lte"] = query.get("datetime") + filter["target_date__lt"] = query.get("datetime") return filter diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 2bc109968..537564828 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,31 +1,31 @@ # base requirements -Django==3.2.19 +Django==4.2.3 django-braces==1.15.0 -django-taggit==3.1.0 -psycopg2==2.9.5 -django-oauth-toolkit==2.2.0 -mistune==2.0.4 +django-taggit==4.0.0 +psycopg2==2.9.6 +django-oauth-toolkit==2.3.0 +mistune==3.0.1 djangorestframework==3.14.0 -redis==4.5.4 +redis==4.6.0 django-nested-admin==4.0.2 -django-cors-headers==3.13.0 -whitenoise==6.3.0 -django-allauth==0.52.0 -faker==13.4.0 -django-filter==22.1 +django-cors-headers==4.1.0 +whitenoise==6.5.0 +django-allauth==0.54.0 +faker==18.11.2 +django-filter==23.2 jsonmodels==2.6.0 djangorestframework-simplejwt==5.2.2 -sentry-sdk==1.14.0 -django-s3-storage==0.13.11 +sentry-sdk==1.27.0 +django-s3-storage==0.14.0 django-crum==0.7.9 django-guardian==2.4.0 dj_rest_auth==2.2.5 -google-auth==2.16.0 -google-api-python-client==2.75.0 -django-redis==5.2.0 -uvicorn==0.20.0 +google-auth==2.21.0 +google-api-python-client==2.92.0 +django-redis==5.3.0 +uvicorn==0.22.0 channels==4.0.0 -openai==0.27.2 -slack-sdk==3.20.2 -celery==5.2.7 \ No newline at end of file +openai==0.27.8 +slack-sdk==3.21.3 +celery==5.3.1 \ No newline at end of file diff --git a/apiserver/requirements/local.txt b/apiserver/requirements/local.txt index efd74a071..426236ed8 100644 --- a/apiserver/requirements/local.txt +++ b/apiserver/requirements/local.txt @@ -1,3 +1,3 @@ -r base.txt -django-debug-toolbar==3.8.1 \ No newline at end of file +django-debug-toolbar==4.1.0 \ No newline at end of file diff --git a/apiserver/requirements/production.txt b/apiserver/requirements/production.txt index c37e98ffd..30d9dc9bb 100644 --- a/apiserver/requirements/production.txt +++ b/apiserver/requirements/production.txt @@ -1,12 +1,11 @@ -r base.txt -dj-database-url==1.2.0 +dj-database-url==2.0.0 gunicorn==20.1.0 -whitenoise==6.3.0 +whitenoise==6.5.0 django-storages==1.13.2 -boto3==1.26.136 -django-anymail==9.0 -twilio==7.16.2 -django-debug-toolbar==3.8.1 +boto3==1.27.0 +django-anymail==10.0 +django-debug-toolbar==4.1.0 gevent==22.10.2 psycogreen==1.0.2 \ No newline at end of file diff --git a/apps/app/Dockerfile.web b/apps/app/Dockerfile.web index 1b9bc41d5..e0b5f29c1 100644 --- a/apps/app/Dockerfile.web +++ b/apps/app/Dockerfile.web @@ -20,7 +20,7 @@ ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 COPY .gitignore .gitignore COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/yarn.lock ./yarn.lock -RUN yarn install +RUN yarn install --network-timeout 500000 # Build the project COPY --from=builder /app/out/full/ . diff --git a/apps/app/components/core/board-view/board-header.tsx b/apps/app/components/core/board-view/board-header.tsx index a5df7a426..30ec5c0fb 100644 --- a/apps/app/components/core/board-view/board-header.tsx +++ b/apps/app/components/core/board-view/board-header.tsx @@ -57,18 +57,6 @@ export const BoardHeader: React.FC = ({ : null ); - let bgColor = "#000000"; - if (selectedGroup === "state") bgColor = currentState?.color ?? "#000000"; - - if (selectedGroup === "priority") - groupTitle === "high" - ? (bgColor = "#dc2626") - : groupTitle === "medium" - ? (bgColor = "#f97316") - : groupTitle === "low" - ? (bgColor = "#22c55e") - : (bgColor = "#ff0000"); - const getGroupTitle = () => { let title = addSpaceIfCamelCase(groupTitle); @@ -96,7 +84,8 @@ export const BoardHeader: React.FC = ({ switch (selectedGroup) { case "state": - icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor); + icon = + currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color); break; case "priority": icon = getPriorityIcon(groupTitle, "text-lg"); diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index cde1b2e38..6753e84cd 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -173,6 +173,9 @@ export const SingleBoardIssue: React.FC = ({ .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .then(() => { mutate(fetchKey); + + if (cycleId) mutate(CYCLE_DETAILS(cycleId as string)); + if (moduleId) mutate(MODULE_DETAILS(moduleId as string)); }); }, [ @@ -368,7 +371,6 @@ export const SingleBoardIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} - tooltipPosition="left" user={user} selfPositioned /> @@ -378,7 +380,6 @@ export const SingleBoardIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} - tooltipPosition="left" user={user} selfPositioned /> diff --git a/apps/app/components/core/calendar-view/calendar.tsx b/apps/app/components/core/calendar-view/calendar.tsx index fa29eb9f7..9da6e5873 100644 --- a/apps/app/components/core/calendar-view/calendar.tsx +++ b/apps/app/components/core/calendar-view/calendar.tsx @@ -170,7 +170,7 @@ export const CalendarView: React.FC = ({ const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; return calendarIssues ? ( -
+
= ({ activities }) => ( activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value; - value = renderShortNumericDateFormat(date as string); + value = renderShortDateWithYearFormat(date as string); } else if (activity.field === "description") { value = "description"; } else if (activity.field === "attachment") { diff --git a/apps/app/components/core/filters/due-date-filter-modal.tsx b/apps/app/components/core/filters/due-date-filter-modal.tsx new file mode 100644 index 000000000..6c6bc4ec6 --- /dev/null +++ b/apps/app/components/core/filters/due-date-filter-modal.tsx @@ -0,0 +1,186 @@ +import { Fragment } from "react"; + +import { useRouter } from "next/router"; + +// react-hook-form +import { Controller, useForm } from "react-hook-form"; +// react-datepicker +import DatePicker from "react-datepicker"; +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// hooks +import useIssuesView from "hooks/use-issues-view"; +// components +import { DueDateFilterSelect } from "./due-date-filter-select"; +// ui +import { PrimaryButton, SecondaryButton } from "components/ui"; +// icons +import { XMarkIcon } from "@heroicons/react/20/solid"; +// helpers +import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; + +type Props = { + isOpen: boolean; + handleClose: () => void; +}; + +type TFormValues = { + filterType: "before" | "after" | "range"; + date1: Date; + date2: Date; +}; + +const defaultValues: TFormValues = { + filterType: "range", + date1: new Date(), + date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()), +}; + +export const DueDateFilterModal: React.FC = ({ isOpen, handleClose }) => { + const { filters, setFilters } = useIssuesView(); + + const router = useRouter(); + const { viewId } = router.query; + + const { handleSubmit, watch, control } = useForm({ + defaultValues, + }); + + const handleFormSubmit = (formData: TFormValues) => { + const { filterType, date1, date2 } = formData; + + if (filterType === "range") { + setFilters( + { target_date: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] }, + !Boolean(viewId) + ); + } else { + const filteredArray = filters?.target_date?.filter((item) => { + if (item?.includes(filterType)) return false; + + return true; + }); + + const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null; + if (filterOne) + setFilters( + { target_date: [filterOne, `${renderDateFormat(date1)};${filterType}`] }, + !Boolean(viewId) + ); + else + setFilters( + { + target_date: [`${renderDateFormat(date1)};${filterType}`], + }, + !Boolean(viewId) + ); + } + handleClose(); + }; + + const isInvalid = + watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false; + + const nextDay = new Date(watch("date1")); + nextDay.setDate(nextDay.getDate() + 1); + + return ( + + + +
+ +
+
+ + +
+
+ ( + + )} + /> + +
+
+ ( + onChange(val)} + dateFormat="dd-MM-yyyy" + calendarClassName="h-full" + inline + /> + )} + /> + {watch("filterType") === "range" && ( + ( + + )} + /> + )} +
+ {watch("filterType") === "range" && ( +
+ After: + {renderShortDateWithYearFormat(watch("date1"))} + Before: + {!isInvalid && {renderShortDateWithYearFormat(watch("date2"))}} +
+ )} +
+ + Cancel + + + Apply + +
+
+
+
+
+
+
+
+ ); +}; diff --git a/apps/app/components/core/filters/due-date-filter-select.tsx b/apps/app/components/core/filters/due-date-filter-select.tsx new file mode 100644 index 000000000..480a77619 --- /dev/null +++ b/apps/app/components/core/filters/due-date-filter-select.tsx @@ -0,0 +1,58 @@ +import React from "react"; + +// ui +import { CustomSelect } from "components/ui"; +// icons +import { CalendarBeforeIcon, CalendarAfterIcon, CalendarMonthIcon } from "components/icons"; +// fetch-keys + +type Props = { + value: string; + onChange: (value: string) => void; +}; + +type DueDate = { + name: string; + value: string; + icon: any; +}; + +const dueDateRange: DueDate[] = [ + { + name: "Due date before", + value: "before", + icon: , + }, + { + name: "Due date after", + value: "after", + icon: , + }, + { + name: "Due date range", + value: "range", + icon: , + }, +]; + +export const DueDateFilterSelect: React.FC = ({ value, onChange }) => ( + + {dueDateRange.find((item) => item.value === value)?.icon} + {dueDateRange.find((item) => item.value === value)?.name} +
+ } + onChange={onChange} + > + {dueDateRange.map((option, index) => ( + + <> + {option.icon} + {option.name} + + + ))} + +); diff --git a/apps/app/components/core/filters-list.tsx b/apps/app/components/core/filters/filters-list.tsx similarity index 86% rename from apps/app/components/core/filters-list.tsx rename to apps/app/components/core/filters/filters-list.tsx index e080c8e9a..7254f8072 100644 --- a/apps/app/components/core/filters-list.tsx +++ b/apps/app/components/core/filters/filters-list.tsx @@ -17,6 +17,7 @@ import stateService from "services/state.service"; // types import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys"; import { IIssueFilterOptions } from "types"; +import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; export const FilterList: React.FC = ({ filters, setFilters }) => { const router = useRouter(); @@ -60,7 +61,7 @@ export const FilterList: React.FC = ({ filters, setFilters }) => { className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1" > - {replaceUnderscoreIfSnakeCase(key)}: + {key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}: {filters[key as keyof IIssueFilterOptions] === null || (filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? ( @@ -299,6 +300,51 @@ export const FilterList: React.FC = ({ filters, setFilters }) => {
+ ) : key === "target_date" ? ( +
+ {filters.target_date?.map((date: string) => { + if (filters.target_date.length <= 0) return null; + + const splitDate = date.split(";"); + + return ( +
+
+ + {splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])} + + + setFilters( + { + target_date: filters.target_date?.filter( + (d: any) => d !== date + ), + }, + !Boolean(viewId) + ) + } + > + + +
+ ); + })} + +
) : ( (filters[key as keyof IIssueFilterOptions] as any)?.join(", ") )} @@ -332,6 +378,7 @@ export const FilterList: React.FC = ({ filters, setFilters }) => { assignees: null, labels: null, created_by: null, + target_date: null, }) } className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs" diff --git a/apps/app/components/core/filters/index.ts b/apps/app/components/core/filters/index.ts new file mode 100644 index 000000000..01c371911 --- /dev/null +++ b/apps/app/components/core/filters/index.ts @@ -0,0 +1,4 @@ +export * from "./due-date-filter-modal"; +export * from "./due-date-filter-select"; +export * from "./filters-list"; +export * from "./issues-view-filter"; diff --git a/apps/app/components/core/issues-view-filter.tsx b/apps/app/components/core/filters/issues-view-filter.tsx similarity index 90% rename from apps/app/components/core/issues-view-filter.tsx rename to apps/app/components/core/filters/issues-view-filter.tsx index 679f6adc3..4452bfb61 100644 --- a/apps/app/components/core/issues-view-filter.tsx +++ b/apps/app/components/core/filters/issues-view-filter.tsx @@ -2,11 +2,12 @@ import React from "react"; import { useRouter } from "next/router"; +// headless ui +import { Popover, Transition } from "@headlessui/react"; // hooks import useIssuesProperties from "hooks/use-issue-properties"; import useIssuesView from "hooks/use-issues-view"; -// headless ui -import { Popover, Transition } from "@headlessui/react"; +import useEstimateOption from "hooks/use-estimate-option"; // components import { SelectFilters } from "components/views"; // ui @@ -17,15 +18,14 @@ import { ListBulletIcon, Squares2X2Icon, CalendarDaysIcon, - ChartBarIcon, } from "@heroicons/react/24/outline"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types import { Properties } from "types"; // constants import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue"; -import useEstimateOption from "hooks/use-estimate-option"; export const IssuesFilterView: React.FC = () => { const router = useRouter(); @@ -109,26 +109,34 @@ export const IssuesFilterView: React.FC = () => { onSelect={(option) => { const key = option.key as keyof typeof filters; - const valueExists = filters[key]?.includes(option.value); + if (key === "target_date") { + const valueExists = checkIfArraysHaveSameElements( + filters.target_date ?? [], + option.value + ); - if (valueExists) { - setFilters( - { - ...(filters ?? {}), - [option.key]: ((filters[key] ?? []) as any[])?.filter( - (val) => val !== option.value - ), - }, - !Boolean(viewId) - ); + setFilters({ + target_date: valueExists ? null : option.value, + }); } else { - setFilters( - { - ...(filters ?? {}), - [option.key]: [...((filters[key] ?? []) as any[]), option.value], - }, - !Boolean(viewId) - ); + const valueExists = filters[key]?.includes(option.value); + + if (valueExists) + setFilters( + { + [option.key]: ((filters[key] ?? []) as any[])?.filter( + (val) => val !== option.value + ), + }, + !Boolean(viewId) + ); + else + setFilters( + { + [option.key]: [...((filters[key] ?? []) as any[]), option.value], + }, + !Boolean(viewId) + ); } }} direction="left" @@ -262,9 +270,9 @@ export const IssuesFilterView: React.FC = () => { if (key === "estimate" && !isEstimateActive) return null; if ( - (issueView === "spreadsheet" && key === "sub_issue_count") || - key === "attachment_count" || - key === "link" + (issueView === "spreadsheet" && key === "attachment_count") || + (issueView === "spreadsheet" && key === "link") || + (issueView === "spreadsheet" && key === "sub_issue_count") ) return null; diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index c50ce7251..1eb52590c 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -1,17 +1,12 @@ export * from "./board-view"; export * from "./calendar-view"; +export * from "./filters"; export * from "./gantt-chart-view"; export * from "./list-view"; +export * from "./modals"; export * from "./spreadsheet-view"; export * from "./sidebar"; -export * from "./bulk-delete-issues-modal"; -export * from "./existing-issues-list-modal"; -export * from "./filters-list"; -export * from "./gpt-assistant-modal"; -export * from "./image-upload-modal"; -export * from "./issues-view-filter"; export * from "./issues-view"; -export * from "./link-modal"; export * from "./image-picker-popover"; export * from "./feeds"; export * from "./theme-switch"; diff --git a/apps/app/components/core/list-view/all-lists.tsx b/apps/app/components/core/list-view/all-lists.tsx index fd063728a..fcedf169a 100644 --- a/apps/app/components/core/list-view/all-lists.tsx +++ b/apps/app/components/core/list-view/all-lists.tsx @@ -38,7 +38,7 @@ export const AllLists: React.FC = ({ return ( <> {groupedByIssues && ( -
+
{Object.keys(groupedByIssues).map((singleGroup) => { const currentState = selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx index f4d749452..774fbb02d 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -147,6 +147,9 @@ export const SingleListIssue: React.FC = ({ .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .then(() => { mutate(fetchKey); + + if (cycleId) mutate(CYCLE_DETAILS(cycleId as string)); + if (moduleId) mutate(MODULE_DETAILS(moduleId as string)); }); }, [ diff --git a/apps/app/components/core/list-view/single-list.tsx b/apps/app/components/core/list-view/single-list.tsx index a8c6e4a51..b76f3ad8a 100644 --- a/apps/app/components/core/list-view/single-list.tsx +++ b/apps/app/components/core/list-view/single-list.tsx @@ -33,7 +33,6 @@ import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; type Props = { type?: "issue" | "cycle" | "module"; currentState?: IState | null; - bgColor?: string; groupTitle: string; groupedByIssues: { [key: string]: IIssue[]; @@ -53,7 +52,6 @@ type Props = { export const SingleList: React.FC = ({ type, currentState, - bgColor, groupTitle, groupedByIssues, selectedGroup, @@ -113,7 +111,8 @@ export const SingleList: React.FC = ({ switch (selectedGroup) { case "state": - icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor); + icon = + currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color); break; case "priority": icon = getPriorityIcon(groupTitle, "text-lg"); diff --git a/apps/app/components/core/bulk-delete-issues-modal.tsx b/apps/app/components/core/modals/bulk-delete-issues-modal.tsx similarity index 100% rename from apps/app/components/core/bulk-delete-issues-modal.tsx rename to apps/app/components/core/modals/bulk-delete-issues-modal.tsx diff --git a/apps/app/components/core/existing-issues-list-modal.tsx b/apps/app/components/core/modals/existing-issues-list-modal.tsx similarity index 100% rename from apps/app/components/core/existing-issues-list-modal.tsx rename to apps/app/components/core/modals/existing-issues-list-modal.tsx diff --git a/apps/app/components/core/gpt-assistant-modal.tsx b/apps/app/components/core/modals/gpt-assistant-modal.tsx similarity index 100% rename from apps/app/components/core/gpt-assistant-modal.tsx rename to apps/app/components/core/modals/gpt-assistant-modal.tsx diff --git a/apps/app/components/core/image-upload-modal.tsx b/apps/app/components/core/modals/image-upload-modal.tsx similarity index 100% rename from apps/app/components/core/image-upload-modal.tsx rename to apps/app/components/core/modals/image-upload-modal.tsx diff --git a/apps/app/components/core/modals/index.ts b/apps/app/components/core/modals/index.ts new file mode 100644 index 000000000..5f55020e4 --- /dev/null +++ b/apps/app/components/core/modals/index.ts @@ -0,0 +1,5 @@ +export * from "./bulk-delete-issues-modal"; +export * from "./existing-issues-list-modal"; +export * from "./gpt-assistant-modal"; +export * from "./image-upload-modal"; +export * from "./link-modal"; diff --git a/apps/app/components/core/link-modal.tsx b/apps/app/components/core/modals/link-modal.tsx similarity index 100% rename from apps/app/components/core/link-modal.tsx rename to apps/app/components/core/modals/link-modal.tsx diff --git a/apps/app/components/core/spreadsheet-view/single-issue.tsx b/apps/app/components/core/spreadsheet-view/single-issue.tsx index 762de2453..ada1e3689 100644 --- a/apps/app/components/core/spreadsheet-view/single-issue.tsx +++ b/apps/app/components/core/spreadsheet-view/single-issue.tsx @@ -30,7 +30,9 @@ import useToast from "hooks/use-toast"; import issuesService from "services/issues.service"; // constant import { + CYCLE_DETAILS, CYCLE_ISSUES_WITH_PARAMS, + MODULE_DETAILS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, SUB_ISSUES, @@ -43,6 +45,7 @@ import { copyTextToClipboard } from "helpers/string.helper"; type Props = { issue: IIssue; + index: number; expanded: boolean; handleToggleExpand: (issueId: string) => void; properties: Properties; @@ -57,6 +60,7 @@ type Props = { export const SingleSpreadsheetIssue: React.FC = ({ issue, + index, expanded, handleToggleExpand, properties, @@ -140,6 +144,9 @@ export const SingleSpreadsheetIssue: React.FC = ({ mutate(SUB_ISSUES(issue.parent as string)); } else { mutate(fetchKey); + + if (cycleId) mutate(CYCLE_DETAILS(cycleId as string)); + if (moduleId) mutate(MODULE_DETAILS(moduleId as string)); } }) .catch((error) => { @@ -165,6 +172,8 @@ export const SingleSpreadsheetIssue: React.FC = ({ const paddingLeft = `${nestingLevel * 68}px`; + const tooltipPosition = index === 0 ? "bottom" : "top"; + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( @@ -241,16 +250,16 @@ export const SingleSpreadsheetIssue: React.FC = ({ )}
-
- {issue.sub_issues_count > 0 && ( + {issue.sub_issues_count > 0 && ( +
- )} -
+
+ )}
@@ -265,6 +274,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + tooltipPosition={tooltipPosition} customButton user={user} isNotAllowed={isNotAllowed} @@ -277,6 +287,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + tooltipPosition={tooltipPosition} noBorder user={user} isNotAllowed={isNotAllowed} @@ -289,6 +300,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + tooltipPosition={tooltipPosition} customButton user={user} isNotAllowed={isNotAllowed} @@ -301,6 +313,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + tooltipPosition={tooltipPosition} customButton user={user} isNotAllowed={isNotAllowed} @@ -313,6 +326,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + tooltipPosition={tooltipPosition} user={user} isNotAllowed={isNotAllowed} /> diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx index c52cfa59a..1e05eba4e 100644 --- a/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx +++ b/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx @@ -10,6 +10,7 @@ import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; type Props = { key: string; issue: IIssue; + index: number; expandedIssues: string[]; setExpandedIssues: React.Dispatch>; properties: Properties; @@ -24,6 +25,7 @@ type Props = { export const SpreadsheetIssues: React.FC = ({ key, + index, issue, expandedIssues, setExpandedIssues, @@ -57,6 +59,7 @@ export const SpreadsheetIssues: React.FC = ({
= ({ = ({ {spreadsheetIssues.map((issue: IIssue, index) => ( = ({ > - {renderShortDate( + {renderShortDateWithYearFormat( new Date( `${watch("start_date") ? watch("start_date") : cycle?.start_date}` ), @@ -366,7 +366,7 @@ export const CycleDetailsSidebar: React.FC = ({ - {renderShortDate( + {renderShortDateWithYearFormat( new Date( `${watch("end_date") ? watch("end_date") : cycle?.end_date}` ), @@ -408,7 +408,11 @@ export const CycleDetailsSidebar: React.FC = ({
-

{cycle.name}

+
+

+ {cycle.name} +

+
{!isCompleted && ( setCycleDeleteModal(true)}> @@ -427,7 +431,7 @@ export const CycleDetailsSidebar: React.FC = ({
- + {cycle.description}
diff --git a/apps/app/components/cycles/single-cycle-list.tsx b/apps/app/components/cycles/single-cycle-list.tsx index fa725b83a..423580383 100644 --- a/apps/app/components/cycles/single-cycle-list.tsx +++ b/apps/app/components/cycles/single-cycle-list.tsx @@ -172,17 +172,19 @@ export const SingleCycleList: React.FC = ({ : "" }`} /> -
+
-

+

{truncateText(cycle.name, 70)}

-

{cycle.description}

+

+ {cycle.description} +

diff --git a/apps/app/components/gantt-chart/blocks/index.tsx b/apps/app/components/gantt-chart/blocks/index.tsx index d5eadf2a0..31e7839cc 100644 --- a/apps/app/components/gantt-chart/blocks/index.tsx +++ b/apps/app/components/gantt-chart/blocks/index.tsx @@ -18,7 +18,7 @@ export const GanttChartBlocks: FC<{ return (
diff --git a/apps/app/components/icons/calendar-after-icon.tsx b/apps/app/components/icons/calendar-after-icon.tsx new file mode 100644 index 000000000..278e500f0 --- /dev/null +++ b/apps/app/components/icons/calendar-after-icon.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const CalendarAfterIcon: React.FC = ({ width = "24", height = "24", className }) => ( + + + + + + + + + + +); \ No newline at end of file diff --git a/apps/app/components/icons/calendar-before-icon.tsx b/apps/app/components/icons/calendar-before-icon.tsx new file mode 100644 index 000000000..f2651c084 --- /dev/null +++ b/apps/app/components/icons/calendar-before-icon.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const CalendarBeforeIcon: React.FC = ({ width = "24", height = "24", className }) => ( + + + + + + + + + + + + + + + + +); diff --git a/apps/app/components/icons/calendar-month-icon.tsx b/apps/app/components/icons/calendar-month-icon.tsx index a9f5042c9..dbfc43c50 100644 --- a/apps/app/components/icons/calendar-month-icon.tsx +++ b/apps/app/components/icons/calendar-month-icon.tsx @@ -3,17 +3,17 @@ import React from "react"; import type { Props } from "./types"; export const CalendarMonthIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); + + + +); diff --git a/apps/app/components/icons/index.ts b/apps/app/components/icons/index.ts index 07ecafd24..db7aad041 100644 --- a/apps/app/components/icons/index.ts +++ b/apps/app/components/icons/index.ts @@ -4,6 +4,8 @@ export * from "./backlog-state-icon"; export * from "./blocked-icon"; export * from "./blocker-icon"; export * from "./bolt-icon"; +export * from "./calendar-before-icon"; +export * from "./calendar-after-icon"; export * from "./calendar-month-icon"; export * from "./cancel-icon"; export * from "./cancelled-state-icon"; diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx index 072647ae3..53d6c2e95 100644 --- a/apps/app/components/inbox/inbox-issue-card.tsx +++ b/apps/app/components/inbox/inbox-issue-card.tsx @@ -14,7 +14,7 @@ import { XCircleIcon, } from "@heroicons/react/24/outline"; // helpers -import { renderShortNumericDateFormat } from "helpers/date-time.helper"; +import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; // types import type { IInboxIssue } from "types"; // constants @@ -72,12 +72,12 @@ export const InboxIssueCard: React.FC = (props) => {
- {renderShortNumericDateFormat(issue.created_at ?? "")} + {renderShortDateWithYearFormat(issue.created_at ?? "")}
diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index a05239a95..46aa4b33f 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -33,7 +33,7 @@ import { XCircleIcon, } from "@heroicons/react/24/outline"; // helpers -import { renderShortNumericDateFormat } from "helpers/date-time.helper"; +import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; // types import type { IInboxIssue, IIssue } from "types"; // fetch-keys @@ -252,13 +252,17 @@ export const InboxMainContent: React.FC = () => { {new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() ? (

This issue was snoozed till{" "} - {renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")} + {renderShortDateWithYearFormat( + issueDetails.issue_inbox[0].snoozed_till ?? "" + )} .

) : (

This issue has been snoozed till{" "} - {renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")} + {renderShortDateWithYearFormat( + issueDetails.issue_inbox[0].snoozed_till ?? "" + )} .

)} diff --git a/apps/app/components/issues/activity.tsx b/apps/app/components/issues/activity.tsx index 8d708b856..e30f00b0b 100644 --- a/apps/app/components/issues/activity.tsx +++ b/apps/app/components/issues/activity.tsx @@ -26,7 +26,7 @@ import { } from "@heroicons/react/24/outline"; import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons"; // helpers -import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper"; +import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper"; // types import { ICurrentUserResponse, IIssueComment, IIssueLabels } from "types"; @@ -299,7 +299,7 @@ export const IssueActivitySection: React.FC = ({ issueId, user }) => { activityItem.new_value && activityItem.new_value !== "" ? activityItem.new_value : activityItem.old_value; - value = renderShortNumericDateFormat(date as string); + value = renderShortDateWithYearFormat(date as string); } else if (activityItem.field === "description") { value = "description"; } else if (activityItem.field === "attachment") { diff --git a/apps/app/components/issues/select/date.tsx b/apps/app/components/issues/select/date.tsx index 4abb03545..26288e11b 100644 --- a/apps/app/components/issues/select/date.tsx +++ b/apps/app/components/issues/select/date.tsx @@ -5,7 +5,7 @@ import { CalendarDaysIcon, XMarkIcon } from "@heroicons/react/24/outline"; // react-datepicker import DatePicker from "react-datepicker"; // import "react-datepicker/dist/react-datepicker.css"; -import { renderDateFormat } from "helpers/date-time.helper"; +import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; type Props = { value: string | null; @@ -20,7 +20,7 @@ export const IssueDateSelect: React.FC = ({ value, onChange }) => ( {value ? ( <> - {value} + {renderShortDateWithYearFormat(value)} diff --git a/apps/app/components/issues/view-select/assignee.tsx b/apps/app/components/issues/view-select/assignee.tsx index 8bfa797fe..cba1db1e7 100644 --- a/apps/app/components/issues/view-select/assignee.tsx +++ b/apps/app/components/issues/view-select/assignee.tsx @@ -20,8 +20,8 @@ type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; + tooltipPosition?: "top" | "bottom"; selfPositioned?: boolean; - tooltipPosition?: "left" | "right"; customButton?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; @@ -32,7 +32,7 @@ export const ViewAssigneeSelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, - tooltipPosition = "right", + tooltipPosition = "top", user, isNotAllowed, customButton = false, @@ -69,7 +69,7 @@ export const ViewAssigneeSelect: React.FC = ({ const assigneeLabel = ( 0 diff --git a/apps/app/components/issues/view-select/due-date.tsx b/apps/app/components/issues/view-select/due-date.tsx index 163816a99..efd568c30 100644 --- a/apps/app/components/issues/view-select/due-date.tsx +++ b/apps/app/components/issues/view-select/due-date.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; // ui import { CustomDatePicker, Tooltip } from "components/ui"; // helpers -import { findHowManyDaysLeft } from "helpers/date-time.helper"; +import { findHowManyDaysLeft, renderShortDateWithYearFormat } from "helpers/date-time.helper"; // services import trackEventServices from "services/track-event.service"; // types @@ -12,6 +12,7 @@ import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + tooltipPosition?: "top" | "bottom"; noBorder?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; @@ -20,6 +21,7 @@ type Props = { export const ViewDueDateSelect: React.FC = ({ issue, partialUpdateIssue, + tooltipPosition = "top", noBorder = false, user, isNotAllowed, @@ -28,7 +30,13 @@ export const ViewDueDateSelect: React.FC = ({ const { workspaceSlug } = router.query; return ( - +
, issue: IIssue) => void; position?: "left" | "right"; + tooltipPosition?: "top" | "bottom"; selfPositioned?: boolean; customButton?: boolean; user: ICurrentUserResponse | undefined; @@ -27,6 +28,7 @@ export const ViewEstimateSelect: React.FC = ({ issue, partialUpdateIssue, position = "left", + tooltipPosition = "top", selfPositioned = false, customButton = false, user, @@ -40,7 +42,7 @@ export const ViewEstimateSelect: React.FC = ({ const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value; const estimateLabels = ( - +
{estimateValue ?? "None"} diff --git a/apps/app/components/issues/view-select/label.tsx b/apps/app/components/issues/view-select/label.tsx index 33df5cf9f..b82b02a0f 100644 --- a/apps/app/components/issues/view-select/label.tsx +++ b/apps/app/components/issues/view-select/label.tsx @@ -22,7 +22,7 @@ type Props = { partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; selfPositioned?: boolean; - tooltipPosition?: "left" | "right"; + tooltipPosition?: "top" | "bottom"; customButton?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; @@ -33,7 +33,7 @@ export const ViewLabelSelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, - tooltipPosition = "right", + tooltipPosition = "top", user, isNotAllowed, customButton = false, @@ -68,7 +68,7 @@ export const ViewLabelSelect: React.FC = ({ const labelsLabel = ( 0 @@ -118,6 +118,8 @@ export const ViewLabelSelect: React.FC = ({ ); + const noResultIcon = ; + return ( <> {projectId && ( @@ -141,6 +143,7 @@ export const ViewLabelSelect: React.FC = ({ disabled={isNotAllowed} selfPositioned={selfPositioned} footerOption={footerOption} + noResultIcon={noResultIcon} dropdownWidth="w-full min-w-[12rem]" /> diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index 0afd2dd98..e7a674ec7 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -19,6 +19,7 @@ type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; + tooltipPosition?: "top" | "bottom"; selfPositioned?: boolean; noBorder?: boolean; user: ICurrentUserResponse | undefined; @@ -29,6 +30,7 @@ export const ViewPrioritySelect: React.FC = ({ issue, partialUpdateIssue, position = "left", + tooltipPosition = "top", selfPositioned = false, noBorder = false, user, @@ -75,7 +77,11 @@ export const ViewPrioritySelect: React.FC = ({ : "border-brand-base" } items-center`} > - + {getPriorityIcon( issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index 5ec0f71c7..4a9f585e2 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -21,6 +21,7 @@ type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; + tooltipPosition?: "top" | "bottom"; selfPositioned?: boolean; customButton?: boolean; user: ICurrentUserResponse | undefined; @@ -31,6 +32,7 @@ export const ViewStateSelect: React.FC = ({ issue, partialUpdateIssue, position = "left", + tooltipPosition = "top", selfPositioned = false, customButton = false, user, @@ -64,6 +66,7 @@ export const ViewStateSelect: React.FC = ({
{selectedOption && diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index de8714968..3b70814f7 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -34,7 +34,7 @@ import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui"; import { ExclamationIcon } from "components/icons"; import { LinkIcon } from "@heroicons/react/20/solid"; // helpers -import { renderDateFormat, renderShortDate } from "helpers/date-time.helper"; +import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper"; // types import { ICurrentUserResponse, IIssue, IModule, ModuleLink } from "types"; @@ -228,7 +228,10 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs > - {renderShortDate(new Date(`${module.start_date}`), "Start date")} + {renderShortDateWithYearFormat( + new Date(`${module.start_date}`), + "Start date" + )} @@ -279,7 +282,10 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs - {renderShortDate(new Date(`${module?.target_date}`), "End date")} + {renderShortDateWithYearFormat( + new Date(`${module?.target_date}`), + "End date" + )} @@ -322,7 +328,11 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs
-

{module.name}

+
+

+ {module.name} +

+
setModuleDeleteModal(true)}> @@ -339,7 +349,7 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs
- + {module.description}
diff --git a/apps/app/components/pages/pages-list/recent-pages-list.tsx b/apps/app/components/pages/pages-list/recent-pages-list.tsx index 44225aee5..ce66a6ce1 100644 --- a/apps/app/components/pages/pages-list/recent-pages-list.tsx +++ b/apps/app/components/pages/pages-list/recent-pages-list.tsx @@ -41,7 +41,7 @@ export const RecentPagesList: React.FC = ({ viewType }) => { if (pages[key].length === 0) return null; return ( -
+

{replaceUnderscoreIfSnakeCase(key)}

diff --git a/apps/app/components/project/single-project-card.tsx b/apps/app/components/project/single-project-card.tsx index 04e56652d..9b7bf1c99 100644 --- a/apps/app/components/project/single-project-card.tsx +++ b/apps/app/components/project/single-project-card.tsx @@ -22,7 +22,7 @@ import { TrashIcon, } from "@heroicons/react/24/outline"; // helpers -import { renderShortNumericDateFormat } from "helpers/date-time.helper"; +import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types import type { IFavoriteProject, IProject } from "types"; @@ -202,13 +202,13 @@ export const SingleProjectCard: React.FC = ({
- {renderShortNumericDateFormat(project.created_at)} + {renderShortDateWithYearFormat(project.created_at)}
{hasJoined ? ( diff --git a/apps/app/components/ui/custom-search-select.tsx b/apps/app/components/ui/custom-search-select.tsx index fb1c0a88c..f9ea3daa1 100644 --- a/apps/app/components/ui/custom-search-select.tsx +++ b/apps/app/components/ui/custom-search-select.tsx @@ -29,6 +29,7 @@ type CustomSearchSelectProps = { selfPositioned?: boolean; multiple?: boolean; footerOption?: JSX.Element; + noResultIcon?: JSX.Element; dropdownWidth?: string; }; export const CustomSearchSelect = ({ @@ -47,6 +48,7 @@ export const CustomSearchSelect = ({ disabled = false, selfPositioned = false, multiple = false, + noResultIcon, footerOption, dropdownWidth, }: CustomSearchSelectProps) => { @@ -171,7 +173,10 @@ export const CustomSearchSelect = ({ )) ) : ( -

No matching results

+ + {noResultIcon && noResultIcon} +

No matching results

+
) ) : (

Loading...

diff --git a/apps/app/components/ui/date.tsx b/apps/app/components/ui/date.tsx index d835ff22b..d5a2cb722 100644 --- a/apps/app/components/ui/date.tsx +++ b/apps/app/components/ui/date.tsx @@ -5,7 +5,7 @@ import { CalendarDaysIcon, XMarkIcon } from "@heroicons/react/24/outline"; // react-datepicker import DatePicker from "react-datepicker"; // import "react-datepicker/dist/react-datepicker.css"; -import { renderDateFormat } from "helpers/date-time.helper"; +import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; type Props = { value: string | null; @@ -21,7 +21,7 @@ export const DateSelect: React.FC = ({ value, onChange, label }) => ( {value ? ( <> - {value} + {renderShortDateWithYearFormat(value)} diff --git a/apps/app/components/ui/datepicker.tsx b/apps/app/components/ui/datepicker.tsx index 80ce7aa91..9b20dcafc 100644 --- a/apps/app/components/ui/datepicker.tsx +++ b/apps/app/components/ui/datepicker.tsx @@ -38,9 +38,9 @@ export const CustomDatePicker: React.FC = ({ }} className={`${ renderAs === "input" - ? "block px-3 py-2 text-sm focus:outline-none" + ? "block px-2 py-2 text-sm focus:outline-none" : renderAs === "button" - ? `px-3 py-1 text-xs shadow-sm ${ + ? `px-2 py-1 text-xs shadow-sm ${ disabled ? "" : "hover:bg-brand-surface-2" } duration-300 focus:border-brand-accent focus:outline-none focus:ring-1 focus:ring-brand-accent` : "" @@ -49,7 +49,7 @@ export const CustomDatePicker: React.FC = ({ } ${ noBorder ? "" : "border border-brand-base" } w-full rounded-md bg-transparent caret-transparent ${className}`} - dateFormat="dd-MM-yyyy" + dateFormat="MMM dd, yyyy" isClearable={isClearable} disabled={disabled} /> diff --git a/apps/app/components/ui/multi-level-dropdown.tsx b/apps/app/components/ui/multi-level-dropdown.tsx index ee096774f..e0d5b0f17 100644 --- a/apps/app/components/ui/multi-level-dropdown.tsx +++ b/apps/app/components/ui/multi-level-dropdown.tsx @@ -18,6 +18,7 @@ type MultiLevelDropdownProps = { label: string | JSX.Element; value: any; selected?: boolean; + element?: JSX.Element; }[]; }[]; onSelect: (value: any) => void; @@ -35,117 +36,121 @@ export const MultiLevelDropdown: React.FC = ({ const [openChildFor, setOpenChildFor] = useState(null); return ( - - {({ open }) => ( - <> -
- setOpenChildFor(null)} - className={`group flex items-center justify-between gap-2 rounded-md border border-brand-base px-3 py-1.5 text-xs shadow-sm duration-300 focus:outline-none ${ - open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary" - }`} + <> + + {({ open }) => ( + <> +
+ setOpenChildFor(null)} + className={`group flex items-center justify-between gap-2 rounded-md border border-brand-base px-3 py-1.5 text-xs shadow-sm duration-300 focus:outline-none ${ + open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary" + }`} + > + {label} + +
+ - {label} -
- - - {options.map((option) => ( -
- { - if (option.children) { - e.stopPropagation(); - e.preventDefault(); + + {options.map((option) => ( +
+ { + if (option.children) { + e.stopPropagation(); + e.preventDefault(); - if (openChildFor === option.id) setOpenChildFor(null); - else setOpenChildFor(option.id); - } else { - onSelect(option.value); - } - }} - className="w-full" - > - {({ active }) => ( - <> -
- {direction === "left" && option.children && ( -
- - )} -
- {option.children && option.id === openChildFor && ( -
-
- {option.children.map((child) => ( - - ))} + {direction === "left" && option.children && ( +
+ + )} + + {option.children && option.id === openChildFor && ( +
+
+ {option.children.map((child) => { + if (child.element) return child.element; + else + return ( + + ); + })} +
-
- )} -
- ))} -
- - - )} -
+ )} +
+ ))} + + + + )} + + ); }; diff --git a/apps/app/components/views/select-filters.tsx b/apps/app/components/views/select-filters.tsx index 164c4f58c..6c2c04954 100644 --- a/apps/app/components/views/select-filters.tsx +++ b/apps/app/components/views/select-filters.tsx @@ -1,3 +1,5 @@ +import { useState } from "react"; + import { useRouter } from "next/router"; import useSWR from "swr"; @@ -6,18 +8,22 @@ import useSWR from "swr"; import stateService from "services/state.service"; import projectService from "services/project.service"; import issuesService from "services/issues.service"; +// components +import { DueDateFilterModal } from "components/core"; // ui import { Avatar, MultiLevelDropdown } from "components/ui"; // icons import { getPriorityIcon, getStateGroupIcon } from "components/icons"; // helpers import { getStatesList } from "helpers/state.helper"; +import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types import { IIssueFilterOptions, IQuery } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys"; // constants import { PRIORITIES } from "constants/project"; +import { DUE_DATES } from "constants/due-dates"; type Props = { filters: Partial | IQuery; @@ -32,6 +38,8 @@ export const SelectFilters: React.FC = ({ direction = "right", height = "md", }) => { + const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false); + const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -58,125 +66,163 @@ export const SelectFilters: React.FC = ({ ); return ( - ({ - id: priority === null ? "null" : priority, - label: ( -
- {getPriorityIcon(priority)} {priority ?? "None"} -
- ), - value: { - key: "priority", - value: priority === null ? "null" : priority, + <> + {isDueDateFilterModalOpen && ( + setIsDueDateFilterModalOpen(false)} + /> + )} + ({ + id: priority === null ? "null" : priority, + label: ( +
+ {getPriorityIcon(priority)} {priority ?? "None"} +
+ ), + value: { + key: "priority", + value: priority === null ? "null" : priority, + }, + selected: filters?.priority?.includes(priority === null ? "null" : priority), + })), + ], + }, + { + id: "state", + label: "State", + value: statesList, + children: [ + ...statesList.map((state) => ({ + id: state.id, + label: ( +
+ {getStateGroupIcon(state.group, "16", "16", state.color)} {state.name} +
+ ), + value: { + key: "state", + value: state.id, + }, + selected: filters?.state?.includes(state.id), + })), + ], + }, + { + id: "assignees", + label: "Assignees", + value: members, + children: [ + ...(members?.map((member) => ({ + id: member.member.id, + label: ( +
+ + {member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email} +
+ ), + value: { + key: "assignees", + value: member.member.id, + }, + selected: filters?.assignees?.includes(member.member.id), + })) ?? []), + ], + }, + { + id: "created_by", + label: "Created by", + value: members, + children: [ + ...(members?.map((member) => ({ + id: member.member.id, + label: ( +
+ + {member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email} +
+ ), + value: { + key: "created_by", + value: member.member.id, + }, + selected: filters?.created_by?.includes(member.member.id), + })) ?? []), + ], + }, + { + id: "labels", + label: "Labels", + value: issueLabels, + children: [ + ...(issueLabels?.map((label) => ({ + id: label.id, + label: ( +
+
+ {label.name} +
+ ), + value: { + key: "labels", + value: label.id, + }, + selected: filters?.labels?.includes(label.id), + })) ?? []), + ], + }, + { + id: "target_date", + label: "Due date", + value: DUE_DATES, + children: [ + ...(DUE_DATES?.map((option) => ({ + id: option.name, + label: option.name, + value: { + key: "target_date", + value: option.value, + }, + selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value), + })) ?? []), + { + id: "custom", + label: "Custom", + value: "custom", + element: ( + + ), }, - selected: filters?.priority?.includes(priority === null ? "null" : priority), - })), - ], - }, - { - id: "state", - label: "State", - value: statesList, - children: [ - ...statesList.map((state) => ({ - id: state.id, - label: ( -
- {getStateGroupIcon(state.group, "16", "16", state.color)} {state.name} -
- ), - value: { - key: "state", - value: state.id, - }, - selected: filters?.state?.includes(state.id), - })), - ], - }, - { - id: "assignees", - label: "Assignees", - value: members, - children: [ - ...(members?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.first_name && member.member.first_name !== "" - ? member.member.first_name - : member.member.email} -
- ), - value: { - key: "assignees", - value: member.member.id, - }, - selected: filters?.assignees?.includes(member.member.id), - })) ?? []), - ], - }, - { - id: "created_by", - label: "Created By", - value: members, - children: [ - ...(members?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.first_name && member.member.first_name !== "" - ? member.member.first_name - : member.member.email} -
- ), - value: { - key: "created_by", - value: member.member.id, - }, - selected: filters?.created_by?.includes(member.member.id), - })) ?? []), - ], - }, - { - id: "labels", - label: "Labels", - value: issueLabels, - children: [ - ...(issueLabels?.map((label) => ({ - id: label.id, - label: ( -
-
- {label.name} -
- ), - value: { - key: "labels", - value: label.id, - }, - selected: filters?.labels?.includes(label.id), - })) ?? []), - ], - }, - ]} - /> + ], + }, + ]} + /> + ); }; diff --git a/apps/app/components/views/single-view-item.tsx b/apps/app/components/views/single-view-item.tsx index 5e5ff3cc0..233ee3e74 100644 --- a/apps/app/components/views/single-view-item.tsx +++ b/apps/app/components/views/single-view-item.tsx @@ -18,7 +18,7 @@ import { VIEWS_LIST } from "constants/fetch-keys"; import useToast from "hooks/use-toast"; // helpers import { truncateText } from "helpers/string.helper"; -import { renderShortDate, renderShortTime } from "helpers/date-time.helper"; +import { renderShortDateWithYearFormat, renderShortTime } from "helpers/date-time.helper"; type Props = { view: IView; @@ -107,7 +107,7 @@ export const SingleViewItem: React.FC = ({ view, handleEditView, handleDe

{renderShortTime(view.updated_at)} diff --git a/apps/app/components/workspace/activity-graph.tsx b/apps/app/components/workspace/activity-graph.tsx index 1f9db203d..b49446eeb 100644 --- a/apps/app/components/workspace/activity-graph.tsx +++ b/apps/app/components/workspace/activity-graph.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; // ui import { Tooltip } from "components/ui"; // helpers -import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-time.helper"; +import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; // types import { IUserActivity } from "types"; // constants @@ -109,7 +109,7 @@ export const ActivityGraph: React.FC = ({ activities }) => { key={`${date}-${index}`} tooltipContent={`${ isActive ? isActive.activity_count : 0 - } activities on ${renderShortNumericDateFormat(date)}`} + } activities on ${renderShortDateWithYearFormat(date)}`} theme="dark" >

= ({ groupedIssues }) => ( })) ?? [] } height="320px" - innerRadius={0.5} - arcLinkLabel={(cell) => `${capitalizeFirstLetter(cell.label.toString())} (${cell.value})`} + innerRadius={0.6} + cornerRadius={5} + padAngle={2} + enableArcLabels + arcLabelsTextColor="#000000" + enableArcLinkLabels={false} legends={[ { anchor: "right", @@ -53,8 +57,14 @@ export const IssuesPieChart: React.FC = ({ groupedIssues }) => ( ]} activeInnerRadiusOffset={5} colors={(datum) => datum.data.color} + tooltip={(datum) => ( +
+ {datum.datum.label} issues:{" "} + {datum.datum.value} +
+ )} theme={{ - background: "rgb(var(--color-bg-base))", + background: "transparent", }} />
diff --git a/apps/app/constants/due-dates.ts b/apps/app/constants/due-dates.ts new file mode 100644 index 000000000..362fd41a5 --- /dev/null +++ b/apps/app/constants/due-dates.ts @@ -0,0 +1,37 @@ +// helper +import { renderDateFormat } from "helpers/date-time.helper"; + +export const DUE_DATES = [ + { + name: "Last week", + value: [ + `${renderDateFormat(new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000))};after`, + `${renderDateFormat(new Date())};before`, + ], + }, + { + name: "2 weeks from now", + value: [ + `${renderDateFormat(new Date())};after`, + `${renderDateFormat(new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000))};before`, + ], + }, + { + name: "1 month from now", + value: [ + `${renderDateFormat(new Date())};after`, + `${renderDateFormat( + new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()) + )};before`, + ], + }, + { + name: "2 months from now", + value: [ + `${renderDateFormat(new Date())};after`, + `${renderDateFormat( + new Date(new Date().getFullYear(), new Date().getMonth() + 2, new Date().getDate()) + )};before`, + ], + }, +]; diff --git a/apps/app/constants/project.ts b/apps/app/constants/project.ts index 9ddae96c8..41688f7a7 100644 --- a/apps/app/constants/project.ts +++ b/apps/app/constants/project.ts @@ -1,3 +1,4 @@ + export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" }; export const GROUP_CHOICES = { diff --git a/apps/app/contexts/issue-view.context.tsx b/apps/app/contexts/issue-view.context.tsx index d2a4496c9..454023d54 100644 --- a/apps/app/contexts/issue-view.context.tsx +++ b/apps/app/contexts/issue-view.context.tsx @@ -89,6 +89,7 @@ export const initialState: StateType = { issue__assignees__id: null, issue__labels__id: null, created_by: null, + target_date: null, }, }; diff --git a/apps/app/helpers/array.helper.ts b/apps/app/helpers/array.helper.ts index 2432f88ad..f8134b440 100644 --- a/apps/app/helpers/array.helper.ts +++ b/apps/app/helpers/array.helper.ts @@ -42,3 +42,11 @@ export const findStringWithMostCharacters = (strings: string[]) => strings.reduce((longestString, currentString) => currentString.length > longestString.length ? currentString : longestString ); + +export const checkIfArraysHaveSameElements = (arr1: any[] | null, arr2: any[] | null): boolean => { + if (!arr1 || !arr2) return false; + if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false; + if (arr1.length === 0 && arr2.length === 0) return true; + + return arr1.length === arr2.length && arr1.every((e) => arr2.includes(e)); +}; diff --git a/apps/app/helpers/date-time.helper.ts b/apps/app/helpers/date-time.helper.ts index d462474a4..ac49dc3ed 100644 --- a/apps/app/helpers/date-time.helper.ts +++ b/apps/app/helpers/date-time.helper.ts @@ -114,7 +114,7 @@ export const getDateRangeStatus = ( } }; -export const renderShortDateWithYearFormat = (date: string | Date) => { +export const renderShortDateWithYearFormat = (date: string | Date, placeholder?: string) => { if (!date || date === "") return null; date = new Date(date); @@ -136,7 +136,8 @@ export const renderShortDateWithYearFormat = (date: string | Date) => { const day = date.getDate(); const month = months[date.getMonth()]; const year = date.getFullYear(); - return isNaN(date.getTime()) ? "N/A" : ` ${month} ${day}, ${year}`; + + return isNaN(date.getTime()) ? placeholder ?? "N/A" : ` ${month} ${day}, ${year}`; }; export const renderShortDate = (date: string | Date, placeholder?: string) => { diff --git a/apps/app/hooks/use-issues-view.tsx b/apps/app/hooks/use-issues-view.tsx index 0e27ed049..cbd550053 100644 --- a/apps/app/hooks/use-issues-view.tsx +++ b/apps/app/hooks/use-issues-view.tsx @@ -60,6 +60,7 @@ const useIssuesView = () => { ? filters?.issue__labels__id.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, }; const { data: projectIssues } = useSWR( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 78af8e9e1..917515931 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -159,7 +159,7 @@ const SingleCycle: React.FC = () => { > setAnalyticsModal(false)} />
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 93bfecfe0..cb57ff9fd 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -120,10 +120,10 @@ const IssueDetailsPage: NextPage = () => { > {issueDetails && projectId ? (
-
+
-
+
{ } > setAnalyticsModal(false)} /> - +
+ +
); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index 51b6b7a5b..8e15cc6f3 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -164,7 +164,7 @@ const SingleModule: React.FC = () => { setAnalyticsModal(false)} />
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index a51c8b44f..f76c7f7e4 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -124,7 +124,29 @@ const ProjectPages: NextPage = () => { } >
-

Pages

+
+

Pages

+
+ + +
+
{ }} > -
+
{tabsList.map((tab, index) => ( { ))}
-
- - -
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx index 080aa9011..b1cbf97f2 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx @@ -101,7 +101,9 @@ const SingleView: React.FC = () => {
} > - +
+ +
); diff --git a/apps/app/styles/globals.css b/apps/app/styles/globals.css index da7f512b9..65cc9aef4 100644 --- a/apps/app/styles/globals.css +++ b/apps/app/styles/globals.css @@ -197,25 +197,6 @@ body { outline: none; } -/* react datepicker styling */ -.react-datepicker-wrapper input::placeholder { - color: rgba(var(--color-text-secondary)); - opacity: 1; -} - -.react-datepicker-wrapper input:-ms-input-placeholder { - color: rgba(var(--color-text-secondary)); -} - -.react-datepicker-wrapper .react-datepicker__close-icon::after { - background: transparent; - color: rgba(var(--color-text-secondary)); -} - -.react-datepicker-popper { - z-index: 30 !important; -} - .conical-gradient { background: conic-gradient( from 180deg at 50% 50%, diff --git a/apps/app/styles/react-datepicker.css b/apps/app/styles/react-datepicker.css index 918f4ed66..2c45fda44 100644 --- a/apps/app/styles/react-datepicker.css +++ b/apps/app/styles/react-datepicker.css @@ -81,7 +81,7 @@ } .react-datepicker__day-name { - color: rgba(var(--color-text-base)) !important; + color: rgba(var(--color-text-secondary)) !important; } .react-datepicker__week { diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index a33a04ffc..aac0ec4eb 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -239,6 +239,7 @@ export interface IIssueLite { export interface IIssueFilterOptions { type: "active" | "backlog" | null; assignees: string[] | null; + target_date: string[] | null; state: string[] | null; labels: string[] | null; issue__assignees__id: string[] | null; diff --git a/apps/app/types/projects.d.ts b/apps/app/types/projects.d.ts index c9972bc62..4a6e5154a 100644 --- a/apps/app/types/projects.d.ts +++ b/apps/app/types/projects.d.ts @@ -80,8 +80,8 @@ type ProjectViewTheme = { export interface IProjectMember { id: string; member: IUserLite; - project: IProject; - workspace: IWorkspace; + project: IProjectLite; + workspace: IWorkspaceLite; comment: string; role: 5 | 10 | 15 | 20; diff --git a/docker-compose.yml b/docker-compose.yml index 640bb723e..496ee434d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,36 @@ version: "3.8" +x-api-and-worker-env: &api-and-worker-env + DEBUG: ${DEBUG} + SENTRY_DSN: ${SENTRY_DSN} + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} + REDIS_URL: redis://plane-redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + EMAIL_PORT: ${EMAIL_PORT} + EMAIL_FROM: ${EMAIL_FROM} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} + EMAIL_USE_SSL: ${EMAIL_USE_SSL} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} + WEB_URL: ${WEB_URL} + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} + DEFAULT_EMAIL: ${DEFAULT_EMAIL} + DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} + USE_MINIO: ${USE_MINIO} + ENABLE_SIGNUP: ${ENABLE_SIGNUP} + services: plane-web: container_name: planefrontend @@ -37,35 +68,7 @@ services: env_file: - .env environment: - DEBUG: ${DEBUG} - SENTRY_DSN: ${SENTRY_DSN} - DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} - REDIS_URL: redis://plane-redis:6379/ - EMAIL_HOST: ${EMAIL_HOST} - EMAIL_HOST_USER: ${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} - EMAIL_PORT: ${EMAIL_PORT} - EMAIL_FROM: ${EMAIL_FROM} - EMAIL_USE_TLS: ${EMAIL_USE_TLS} - EMAIL_USE_SSL: ${EMAIL_USE_SSL} - AWS_REGION: ${AWS_REGION} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} - FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} - WEB_URL: ${WEB_URL} - GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} - DISABLE_COLLECTSTATIC: 1 - DOCKERIZED: 1 - OPENAI_API_KEY: ${OPENAI_API_KEY} - GPT_ENGINE: ${GPT_ENGINE} - SECRET_KEY: ${SECRET_KEY} - DEFAULT_EMAIL: ${DEFAULT_EMAIL} - DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} - USE_MINIO: ${USE_MINIO} - ENABLE_SIGNUP: ${ENABLE_SIGNUP} + <<: *api-and-worker-env depends_on: - plane-db - plane-redis @@ -80,35 +83,7 @@ services: env_file: - .env environment: - DEBUG: ${DEBUG} - SENTRY_DSN: ${SENTRY_DSN} - DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} - REDIS_URL: redis://plane-redis:6379/ - EMAIL_HOST: ${EMAIL_HOST} - EMAIL_HOST_USER: ${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} - EMAIL_PORT: ${EMAIL_PORT} - EMAIL_FROM: ${EMAIL_FROM} - EMAIL_USE_TLS: ${EMAIL_USE_TLS} - EMAIL_USE_SSL: ${EMAIL_USE_SSL} - AWS_REGION: ${AWS_REGION} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} - FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} - WEB_URL: ${WEB_URL} - GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} - DISABLE_COLLECTSTATIC: 1 - DOCKERIZED: 1 - OPENAI_API_KEY: ${OPENAI_API_KEY} - GPT_ENGINE: ${GPT_ENGINE} - SECRET_KEY: ${SECRET_KEY} - DEFAULT_EMAIL: ${DEFAULT_EMAIL:-captain@plane.so} - DEFAULT_PASSWORD: ${DEFAULT_PASSWORD:-password123} - USE_MINIO: ${USE_MINIO} - ENABLE_SIGNUP: ${ENABLE_SIGNUP} + <<: *api-and-worker-env depends_on: - plane-api - plane-db