mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of https://github.com/makeplane/plane into chore/update_theming
This commit is contained in:
commit
9ba72d9de3
@ -23,7 +23,6 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
|
|||||||
- Python version 3.8+
|
- Python version 3.8+
|
||||||
- Postgres version v14
|
- Postgres version v14
|
||||||
- Redis version v6.2.7
|
- Redis version v6.2.7
|
||||||
- pnpm version 7.22.0
|
|
||||||
|
|
||||||
### Setup the project
|
### Setup the project
|
||||||
|
|
||||||
|
@ -61,14 +61,6 @@ chmod +x setup.sh
|
|||||||
|
|
||||||
> If running in a cloud env replace localhost with public facing IP address of the VM
|
> 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
|
- Run Docker compose up
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -77,6 +77,13 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
raise serializers.ValidationError(detail="Project Identifier is already taken")
|
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):
|
class ProjectDetailSerializer(BaseSerializer):
|
||||||
workspace = WorkSpaceSerializer(read_only=True)
|
workspace = WorkSpaceSerializer(read_only=True)
|
||||||
default_assignee = UserLiteSerializer(read_only=True)
|
default_assignee = UserLiteSerializer(read_only=True)
|
||||||
@ -94,7 +101,7 @@ class ProjectDetailSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class ProjectMemberSerializer(BaseSerializer):
|
class ProjectMemberSerializer(BaseSerializer):
|
||||||
workspace = WorkSpaceSerializer(read_only=True)
|
workspace = WorkSpaceSerializer(read_only=True)
|
||||||
project = ProjectSerializer(read_only=True)
|
project = ProjectLiteSerializer(read_only=True)
|
||||||
member = UserLiteSerializer(read_only=True)
|
member = UserLiteSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -127,10 +134,3 @@ class ProjectFavoriteSerializer(BaseSerializer):
|
|||||||
"workspace",
|
"workspace",
|
||||||
"user",
|
"user",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProjectLiteSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Project
|
|
||||||
fields = ["id", "identifier", "name"]
|
|
||||||
read_only_fields = fields
|
|
||||||
|
@ -295,7 +295,6 @@ urlpatterns = [
|
|||||||
{
|
{
|
||||||
"delete": "destroy",
|
"delete": "destroy",
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
"get": "retrieve",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="workspace",
|
name="workspace",
|
||||||
|
@ -256,7 +256,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
print(e)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
@ -259,7 +259,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
group="backlog",
|
group="backlog",
|
||||||
description="Default state for managing all Inbox Issues",
|
description="Default state for managing all Inbox Issues",
|
||||||
project_id=pk,
|
project_id=pk,
|
||||||
color="#ff7700"
|
color="#ff7700",
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -550,45 +550,47 @@ class AddMemberToProjectEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
member_id = request.data.get("member_id", False)
|
members = request.data.get("members", [])
|
||||||
role = request.data.get("role", False)
|
|
||||||
|
|
||||||
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(
|
return Response(
|
||||||
{"error": "Member ID and role is required"},
|
{"error": "Atleast one member is required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if the user is a member in the workspace
|
project_members = ProjectMember.objects.bulk_create(
|
||||||
if not WorkspaceMember.objects.filter(
|
[
|
||||||
workspace__slug=slug, member_id=member_id
|
ProjectMember(
|
||||||
).exists():
|
member_id=member.get("member_id"),
|
||||||
# TODO: Update this error message - nk
|
role=member.get("role", 10),
|
||||||
return Response(
|
project_id=project_id,
|
||||||
{
|
workspace_id=project.workspace_id,
|
||||||
"error": "User is not a member of the workspace. Invite the user to the workspace to add him to project"
|
)
|
||||||
},
|
for member in members
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
],
|
||||||
)
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
# 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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = ProjectMemberSerializer(project_member)
|
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
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:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -3,6 +3,7 @@ import jwt
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Prefetch
|
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.functions import ExtractWeek, Cast, ExtractDay
|
||||||
from django.db.models.fields import DateField
|
from django.db.models.fields import DateField
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
# Third party modules
|
# Third party modules
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -93,14 +95,33 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
return self.filter_queryset(
|
return (
|
||||||
super().get_queryset().select_related("owner")
|
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)
|
.order_by("name")
|
||||||
|
.filter(workspace_member__member=self.request.user)
|
||||||
|
.annotate(total_members=member_count)
|
||||||
|
.annotate(total_issues=issue_count)
|
||||||
|
)
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
try:
|
try:
|
||||||
serializer = WorkSpaceSerializer(data=request.data)
|
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():
|
if serializer.is_valid():
|
||||||
serializer.save(owner=request.user)
|
serializer.save(owner=request.user)
|
||||||
# Create Workspace member
|
# Create Workspace member
|
||||||
@ -160,14 +181,20 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
workspace = (
|
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(
|
.annotate(total_members=member_count)
|
||||||
workspace_member__member=request.user,
|
.annotate(total_issues=issue_count)
|
||||||
)
|
)
|
||||||
.select_related("owner")
|
|
||||||
).annotate(total_members=member_count).annotate(total_issues=issue_count)
|
|
||||||
|
|
||||||
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
|
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -216,9 +243,20 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# check for role level
|
# check for role level
|
||||||
requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user)
|
requesting_user = WorkspaceMember.objects.get(
|
||||||
if len([email for email in emails if int(email.get("role", 10)) > requesting_user.role]):
|
workspace__slug=slug, member=request.user
|
||||||
return Response({"error": "You cannot invite a user with higher role"}, status=status.HTTP_400_BAD_REQUEST)
|
)
|
||||||
|
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)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
@ -276,14 +314,18 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# create the user if signup is disabled
|
# create the user if signup is disabled
|
||||||
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
|
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
|
||||||
_ = User.objects.bulk_create([
|
_ = User.objects.bulk_create(
|
||||||
User(
|
[
|
||||||
email=email.get("email"),
|
User(
|
||||||
password=str(uuid4().hex),
|
username=str(uuid4().hex),
|
||||||
is_password_autoset=True
|
email=invitation.email,
|
||||||
)
|
password=make_password(uuid4().hex),
|
||||||
for email in emails
|
is_password_autoset=True,
|
||||||
], batch_size=100)
|
)
|
||||||
|
for invitation in workspace_invitations
|
||||||
|
],
|
||||||
|
batch_size=100,
|
||||||
|
)
|
||||||
|
|
||||||
for invitation in workspace_invitations:
|
for invitation in workspace_invitations:
|
||||||
workspace_invitation.delay(
|
workspace_invitation.delay(
|
||||||
@ -400,6 +442,30 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
|||||||
.select_related("workspace", "workspace__owner", "created_by")
|
.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):
|
class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
||||||
serializer_class = WorkSpaceMemberInviteSerializer
|
serializer_class = WorkSpaceMemberInviteSerializer
|
||||||
@ -865,7 +931,9 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
state_distribution = (
|
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"))
|
.annotate(state_group=F("state__group"))
|
||||||
.values("state_group")
|
.values("state_group")
|
||||||
.annotate(state_count=Count("state_group"))
|
.annotate(state_count=Count("state_group"))
|
||||||
|
42
apiserver/plane/db/migrations/0035_auto_20230704_2225.py
Normal file
42
apiserver/plane/db/migrations/0035_auto_20230704_2225.py
Normal file
@ -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",
|
||||||
|
),
|
||||||
|
]
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -15,15 +15,15 @@ ROLE_CHOICES = (
|
|||||||
|
|
||||||
|
|
||||||
class Workspace(BaseModel):
|
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)
|
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="owner_workspace",
|
related_name="owner_workspace",
|
||||||
)
|
)
|
||||||
slug = models.SlugField(max_length=100, db_index=True, unique=True)
|
slug = models.SlugField(max_length=48, db_index=True, unique=True)
|
||||||
company_size = models.PositiveIntegerField(default=10)
|
organization_size = models.CharField(max_length=20)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return name of the Workspace"""
|
"""Return name of the Workspace"""
|
||||||
|
@ -63,6 +63,7 @@ if os.environ.get("SENTRY_DSN", False):
|
|||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
environment="local",
|
environment="local",
|
||||||
traces_sample_rate=0.7,
|
traces_sample_rate=0.7,
|
||||||
|
profiles_sample_rate=1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
REDIS_HOST = "localhost"
|
REDIS_HOST = "localhost"
|
||||||
|
@ -13,9 +13,7 @@ from sentry_sdk.integrations.redis import RedisIntegration
|
|||||||
from .common import * # noqa
|
from .common import * # noqa
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DEBUG = int(os.environ.get(
|
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
|
||||||
"DEBUG", 0
|
|
||||||
)) == 1
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
@ -84,11 +82,12 @@ if bool(os.environ.get("SENTRY_DSN", False)):
|
|||||||
traces_sample_rate=1,
|
traces_sample_rate=1,
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
environment="production",
|
environment="production",
|
||||||
|
profiles_sample_rate=1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
if DOCKERIZED and USE_MINIO:
|
if DOCKERIZED and USE_MINIO:
|
||||||
INSTALLED_APPS += ("storages",)
|
INSTALLED_APPS += ("storages",)
|
||||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
STORAGES = {"default": {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}}
|
||||||
# The AWS access key to use.
|
# The AWS access key to use.
|
||||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
||||||
# The AWS secret access key to use.
|
# 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.
|
# The name of the bucket to store files in.
|
||||||
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
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.
|
# 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
|
# Default permissions
|
||||||
AWS_DEFAULT_ACL = "public-read"
|
AWS_DEFAULT_ACL = "public-read"
|
||||||
AWS_QUERYSTRING_AUTH = False
|
AWS_QUERYSTRING_AUTH = False
|
||||||
|
@ -66,6 +66,7 @@ sentry_sdk.init(
|
|||||||
traces_sample_rate=1,
|
traces_sample_rate=1,
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
environment="staging",
|
environment="staging",
|
||||||
|
profiles_sample_rate=1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# The AWS region to connect to.
|
# The AWS region to connect to.
|
||||||
|
@ -3,11 +3,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# from django.contrib import admin
|
# 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.views.generic import TemplateView
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url, static
|
|
||||||
|
|
||||||
# from django.conf.urls.static import static
|
# from django.conf.urls.static import static
|
||||||
|
|
||||||
@ -18,11 +17,10 @@ urlpatterns = [
|
|||||||
path("", include("plane.web.urls")),
|
path("", include("plane.web.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = urlpatterns + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^__debug__/", include(debug_toolbar.urls)),
|
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||||
] + urlpatterns
|
] + urlpatterns
|
||||||
|
@ -166,16 +166,16 @@ def filter_target_date(params, filter, method):
|
|||||||
for query in target_dates:
|
for query in target_dates:
|
||||||
target_date_query = query.split(";")
|
target_date_query = query.split(";")
|
||||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
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:
|
else:
|
||||||
filter["target_date__lte"] = target_date_query[0]
|
filter["target_date__lt"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
if params.get("target_date", None) and len(params.get("target_date")):
|
if params.get("target_date", None) and len(params.get("target_date")):
|
||||||
for query in params.get("target_date"):
|
for query in params.get("target_date"):
|
||||||
if query.get("timeline", "after") == "after":
|
if query.get("timeline", "after") == "after":
|
||||||
filter["target_date__gte"] = query.get("datetime")
|
filter["target_date__gt"] = query.get("datetime")
|
||||||
else:
|
else:
|
||||||
filter["target_date__lte"] = query.get("datetime")
|
filter["target_date__lt"] = query.get("datetime")
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
# base requirements
|
# base requirements
|
||||||
|
|
||||||
Django==3.2.19
|
Django==4.2.3
|
||||||
django-braces==1.15.0
|
django-braces==1.15.0
|
||||||
django-taggit==3.1.0
|
django-taggit==4.0.0
|
||||||
psycopg2==2.9.5
|
psycopg2==2.9.6
|
||||||
django-oauth-toolkit==2.2.0
|
django-oauth-toolkit==2.3.0
|
||||||
mistune==2.0.4
|
mistune==3.0.1
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
redis==4.5.4
|
redis==4.6.0
|
||||||
django-nested-admin==4.0.2
|
django-nested-admin==4.0.2
|
||||||
django-cors-headers==3.13.0
|
django-cors-headers==4.1.0
|
||||||
whitenoise==6.3.0
|
whitenoise==6.5.0
|
||||||
django-allauth==0.52.0
|
django-allauth==0.54.0
|
||||||
faker==13.4.0
|
faker==18.11.2
|
||||||
django-filter==22.1
|
django-filter==23.2
|
||||||
jsonmodels==2.6.0
|
jsonmodels==2.6.0
|
||||||
djangorestframework-simplejwt==5.2.2
|
djangorestframework-simplejwt==5.2.2
|
||||||
sentry-sdk==1.14.0
|
sentry-sdk==1.27.0
|
||||||
django-s3-storage==0.13.11
|
django-s3-storage==0.14.0
|
||||||
django-crum==0.7.9
|
django-crum==0.7.9
|
||||||
django-guardian==2.4.0
|
django-guardian==2.4.0
|
||||||
dj_rest_auth==2.2.5
|
dj_rest_auth==2.2.5
|
||||||
google-auth==2.16.0
|
google-auth==2.21.0
|
||||||
google-api-python-client==2.75.0
|
google-api-python-client==2.92.0
|
||||||
django-redis==5.2.0
|
django-redis==5.3.0
|
||||||
uvicorn==0.20.0
|
uvicorn==0.22.0
|
||||||
channels==4.0.0
|
channels==4.0.0
|
||||||
openai==0.27.2
|
openai==0.27.8
|
||||||
slack-sdk==3.20.2
|
slack-sdk==3.21.3
|
||||||
celery==5.2.7
|
celery==5.3.1
|
@ -1,3 +1,3 @@
|
|||||||
-r base.txt
|
-r base.txt
|
||||||
|
|
||||||
django-debug-toolbar==3.8.1
|
django-debug-toolbar==4.1.0
|
@ -1,12 +1,11 @@
|
|||||||
-r base.txt
|
-r base.txt
|
||||||
|
|
||||||
dj-database-url==1.2.0
|
dj-database-url==2.0.0
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
whitenoise==6.3.0
|
whitenoise==6.5.0
|
||||||
django-storages==1.13.2
|
django-storages==1.13.2
|
||||||
boto3==1.26.136
|
boto3==1.27.0
|
||||||
django-anymail==9.0
|
django-anymail==10.0
|
||||||
twilio==7.16.2
|
django-debug-toolbar==4.1.0
|
||||||
django-debug-toolbar==3.8.1
|
|
||||||
gevent==22.10.2
|
gevent==22.10.2
|
||||||
psycogreen==1.0.2
|
psycogreen==1.0.2
|
@ -20,7 +20,7 @@ ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
|||||||
COPY .gitignore .gitignore
|
COPY .gitignore .gitignore
|
||||||
COPY --from=builder /app/out/json/ .
|
COPY --from=builder /app/out/json/ .
|
||||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||||
RUN yarn install
|
RUN yarn install --network-timeout 500000
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
|
@ -57,18 +57,6 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
: null
|
: 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 = () => {
|
const getGroupTitle = () => {
|
||||||
let title = addSpaceIfCamelCase(groupTitle);
|
let title = addSpaceIfCamelCase(groupTitle);
|
||||||
|
|
||||||
@ -96,7 +84,8 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
|
|
||||||
switch (selectedGroup) {
|
switch (selectedGroup) {
|
||||||
case "state":
|
case "state":
|
||||||
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
|
icon =
|
||||||
|
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||||
break;
|
break;
|
||||||
case "priority":
|
case "priority":
|
||||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||||
|
@ -173,6 +173,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
|
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(fetchKey);
|
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<Props> = ({
|
|||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
tooltipPosition="left"
|
|
||||||
user={user}
|
user={user}
|
||||||
selfPositioned
|
selfPositioned
|
||||||
/>
|
/>
|
||||||
@ -378,7 +380,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
tooltipPosition="left"
|
|
||||||
user={user}
|
user={user}
|
||||||
selfPositioned
|
selfPositioned
|
||||||
/>
|
/>
|
||||||
|
@ -170,7 +170,7 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
||||||
|
|
||||||
return calendarIssues ? (
|
return calendarIssues ? (
|
||||||
<div className="h-full">
|
<div className="h-full overflow-y-auto">
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<div className="h-full rounded-lg p-8 text-brand-secondary">
|
<div className="h-full rounded-lg p-8 text-brand-secondary">
|
||||||
<CalendarHeader
|
<CalendarHeader
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
|
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import RemirrorRichTextEditor from "components/rich-text-editor";
|
import RemirrorRichTextEditor from "components/rich-text-editor";
|
||||||
@ -187,7 +187,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
activity.new_value && activity.new_value !== ""
|
activity.new_value && activity.new_value !== ""
|
||||||
? activity.new_value
|
? activity.new_value
|
||||||
: activity.old_value;
|
: activity.old_value;
|
||||||
value = renderShortNumericDateFormat(date as string);
|
value = renderShortDateWithYearFormat(date as string);
|
||||||
} else if (activity.field === "description") {
|
} else if (activity.field === "description") {
|
||||||
value = "description";
|
value = "description";
|
||||||
} else if (activity.field === "attachment") {
|
} else if (activity.field === "attachment") {
|
||||||
|
186
apps/app/components/core/filters/due-date-filter-modal.tsx
Normal file
186
apps/app/components/core/filters/due-date-filter-modal.tsx
Normal file
@ -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<Props> = ({ isOpen, handleClose }) => {
|
||||||
|
const { filters, setFilters } = useIssuesView();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { viewId } = router.query;
|
||||||
|
|
||||||
|
const { handleSubmit, watch, control } = useForm<TFormValues>({
|
||||||
|
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 (
|
||||||
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
<div className="fixed inset-0 z-20 flex w-full justify-center overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative flex transform rounded-lg border border-brand-base bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
|
<form className="space-y-4" onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="filterType"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<DueDateFilterSelect value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<XMarkIcon
|
||||||
|
className="border-base h-4 w-4 cursor-pointer"
|
||||||
|
onClick={handleClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-between gap-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="date1"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<DatePicker
|
||||||
|
selected={value}
|
||||||
|
onChange={(val) => onChange(val)}
|
||||||
|
dateFormat="dd-MM-yyyy"
|
||||||
|
calendarClassName="h-full"
|
||||||
|
inline
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{watch("filterType") === "range" && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="date2"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<DatePicker
|
||||||
|
selected={value}
|
||||||
|
onChange={onChange}
|
||||||
|
dateFormat="dd-MM-yyyy"
|
||||||
|
calendarClassName="h-full"
|
||||||
|
minDate={nextDay}
|
||||||
|
inline
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{watch("filterType") === "range" && (
|
||||||
|
<h6 className="text-xs flex items-center gap-1">
|
||||||
|
<span className="text-brand-secondary">After:</span>
|
||||||
|
<span>{renderShortDateWithYearFormat(watch("date1"))}</span>
|
||||||
|
<span className="text-brand-secondary ml-1">Before:</span>
|
||||||
|
{!isInvalid && <span>{renderShortDateWithYearFormat(watch("date2"))}</span>}
|
||||||
|
</h6>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<SecondaryButton className="flex items-center gap-2" onClick={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
type="submit"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
disabled={isInvalid}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
58
apps/app/components/core/filters/due-date-filter-select.tsx
Normal file
58
apps/app/components/core/filters/due-date-filter-select.tsx
Normal file
@ -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: <CalendarBeforeIcon className="h-4 w-4 " />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Due date after",
|
||||||
|
value: "after",
|
||||||
|
icon: <CalendarAfterIcon className="h-4 w-4 " />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Due date range",
|
||||||
|
value: "range",
|
||||||
|
icon: <CalendarMonthIcon className="h-4 w-4 " />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
|
||||||
|
<CustomSelect
|
||||||
|
value={value}
|
||||||
|
label={
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
{dueDateRange.find((item) => item.value === value)?.icon}
|
||||||
|
<span>{dueDateRange.find((item) => item.value === value)?.name}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
{dueDateRange.map((option, index) => (
|
||||||
|
<CustomSelect.Option key={index} value={option.value}>
|
||||||
|
<>
|
||||||
|
<span>{option.icon}</span>
|
||||||
|
{option.name}
|
||||||
|
</>
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
);
|
@ -17,6 +17,7 @@ import stateService from "services/state.service";
|
|||||||
// types
|
// types
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
|
||||||
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -60,7 +61,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1"
|
className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1"
|
||||||
>
|
>
|
||||||
<span className="capitalize text-brand-secondary">
|
<span className="capitalize text-brand-secondary">
|
||||||
{replaceUnderscoreIfSnakeCase(key)}:
|
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||||
</span>
|
</span>
|
||||||
{filters[key as keyof IIssueFilterOptions] === null ||
|
{filters[key as keyof IIssueFilterOptions] === null ||
|
||||||
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
|
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
|
||||||
@ -299,6 +300,51 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
<XMarkIcon className="h-3 w-3" />
|
<XMarkIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : key === "target_date" ? (
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{filters.target_date?.map((date: string) => {
|
||||||
|
if (filters.target_date.length <= 0) return null;
|
||||||
|
|
||||||
|
const splitDate = date.split(";");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={date}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-base px-1 py-0.5"
|
||||||
|
>
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full" />
|
||||||
|
<span className="capitalize">
|
||||||
|
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters(
|
||||||
|
{
|
||||||
|
target_date: filters.target_date?.filter(
|
||||||
|
(d: any) => d !== date
|
||||||
|
),
|
||||||
|
},
|
||||||
|
!Boolean(viewId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
target_date: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
(filters[key as keyof IIssueFilterOptions] as any)?.join(", ")
|
(filters[key as keyof IIssueFilterOptions] as any)?.join(", ")
|
||||||
)}
|
)}
|
||||||
@ -332,6 +378,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
assignees: null,
|
assignees: null,
|
||||||
labels: null,
|
labels: null,
|
||||||
created_by: 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"
|
className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs"
|
4
apps/app/components/core/filters/index.ts
Normal file
4
apps/app/components/core/filters/index.ts
Normal file
@ -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";
|
@ -2,11 +2,12 @@ import React from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// headless ui
|
import useEstimateOption from "hooks/use-estimate-option";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
|
||||||
// components
|
// components
|
||||||
import { SelectFilters } from "components/views";
|
import { SelectFilters } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
@ -17,15 +18,14 @@ import {
|
|||||||
ListBulletIcon,
|
ListBulletIcon,
|
||||||
Squares2X2Icon,
|
Squares2X2Icon,
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
ChartBarIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { Properties } from "types";
|
import { Properties } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
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 = () => {
|
export const IssuesFilterView: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -109,26 +109,34 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
const key = option.key as keyof typeof filters;
|
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({
|
||||||
setFilters(
|
target_date: valueExists ? null : option.value,
|
||||||
{
|
});
|
||||||
...(filters ?? {}),
|
|
||||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
|
||||||
(val) => val !== option.value
|
|
||||||
),
|
|
||||||
},
|
|
||||||
!Boolean(viewId)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setFilters(
|
const valueExists = filters[key]?.includes(option.value);
|
||||||
{
|
|
||||||
...(filters ?? {}),
|
if (valueExists)
|
||||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
setFilters(
|
||||||
},
|
{
|
||||||
!Boolean(viewId)
|
[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"
|
direction="left"
|
||||||
@ -262,9 +270,9 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
if (key === "estimate" && !isEstimateActive) return null;
|
if (key === "estimate" && !isEstimateActive) return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(issueView === "spreadsheet" && key === "sub_issue_count") ||
|
(issueView === "spreadsheet" && key === "attachment_count") ||
|
||||||
key === "attachment_count" ||
|
(issueView === "spreadsheet" && key === "link") ||
|
||||||
key === "link"
|
(issueView === "spreadsheet" && key === "sub_issue_count")
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1,17 +1,12 @@
|
|||||||
export * from "./board-view";
|
export * from "./board-view";
|
||||||
export * from "./calendar-view";
|
export * from "./calendar-view";
|
||||||
|
export * from "./filters";
|
||||||
export * from "./gantt-chart-view";
|
export * from "./gantt-chart-view";
|
||||||
export * from "./list-view";
|
export * from "./list-view";
|
||||||
|
export * from "./modals";
|
||||||
export * from "./spreadsheet-view";
|
export * from "./spreadsheet-view";
|
||||||
export * from "./sidebar";
|
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 "./issues-view";
|
||||||
export * from "./link-modal";
|
|
||||||
export * from "./image-picker-popover";
|
export * from "./image-picker-popover";
|
||||||
export * from "./feeds";
|
export * from "./feeds";
|
||||||
export * from "./theme-switch";
|
export * from "./theme-switch";
|
||||||
|
@ -38,7 +38,7 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{groupedByIssues && (
|
{groupedByIssues && (
|
||||||
<div>
|
<div className="h-full overflow-y-auto">
|
||||||
{Object.keys(groupedByIssues).map((singleGroup) => {
|
{Object.keys(groupedByIssues).map((singleGroup) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
|
@ -147,6 +147,9 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
|
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(fetchKey);
|
mutate(fetchKey);
|
||||||
|
|
||||||
|
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||||
|
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
@ -33,7 +33,6 @@ import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
|||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
type?: "issue" | "cycle" | "module";
|
||||||
currentState?: IState | null;
|
currentState?: IState | null;
|
||||||
bgColor?: string;
|
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
groupedByIssues: {
|
groupedByIssues: {
|
||||||
[key: string]: IIssue[];
|
[key: string]: IIssue[];
|
||||||
@ -53,7 +52,6 @@ type Props = {
|
|||||||
export const SingleList: React.FC<Props> = ({
|
export const SingleList: React.FC<Props> = ({
|
||||||
type,
|
type,
|
||||||
currentState,
|
currentState,
|
||||||
bgColor,
|
|
||||||
groupTitle,
|
groupTitle,
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
@ -113,7 +111,8 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
|
|
||||||
switch (selectedGroup) {
|
switch (selectedGroup) {
|
||||||
case "state":
|
case "state":
|
||||||
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
|
icon =
|
||||||
|
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||||
break;
|
break;
|
||||||
case "priority":
|
case "priority":
|
||||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||||
|
5
apps/app/components/core/modals/index.ts
Normal file
5
apps/app/components/core/modals/index.ts
Normal file
@ -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";
|
@ -30,7 +30,9 @@ import useToast from "hooks/use-toast";
|
|||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// constant
|
// constant
|
||||||
import {
|
import {
|
||||||
|
CYCLE_DETAILS,
|
||||||
CYCLE_ISSUES_WITH_PARAMS,
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_DETAILS,
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
SUB_ISSUES,
|
SUB_ISSUES,
|
||||||
@ -43,6 +45,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
|
index: number;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
handleToggleExpand: (issueId: string) => void;
|
handleToggleExpand: (issueId: string) => void;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
@ -57,6 +60,7 @@ type Props = {
|
|||||||
|
|
||||||
export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||||
issue,
|
issue,
|
||||||
|
index,
|
||||||
expanded,
|
expanded,
|
||||||
handleToggleExpand,
|
handleToggleExpand,
|
||||||
properties,
|
properties,
|
||||||
@ -140,6 +144,9 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
mutate(SUB_ISSUES(issue.parent as string));
|
mutate(SUB_ISSUES(issue.parent as string));
|
||||||
} else {
|
} else {
|
||||||
mutate(fetchKey);
|
mutate(fetchKey);
|
||||||
|
|
||||||
|
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||||
|
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -165,6 +172,8 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const paddingLeft = `${nestingLevel * 68}px`;
|
const paddingLeft = `${nestingLevel * 68}px`;
|
||||||
|
|
||||||
|
const tooltipPosition = index === 0 ? "bottom" : "top";
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -241,16 +250,16 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-6 w-6 flex justify-center items-center">
|
{issue.sub_issues_count > 0 && (
|
||||||
{issue.sub_issues_count > 0 && (
|
<div className="h-6 w-6 flex justify-center items-center">
|
||||||
<button
|
<button
|
||||||
className="h-5 w-5 hover:bg-brand-surface-1 hover:text-brand-base rounded-sm cursor-pointer"
|
className="h-5 w-5 hover:bg-brand-surface-1 hover:text-brand-base rounded-sm cursor-pointer"
|
||||||
onClick={() => handleToggleExpand(issue.id)}
|
onClick={() => handleToggleExpand(issue.id)}
|
||||||
>
|
>
|
||||||
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
|
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
||||||
@ -265,6 +274,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
position="left"
|
position="left"
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
customButton
|
customButton
|
||||||
user={user}
|
user={user}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
@ -277,6 +287,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
position="left"
|
position="left"
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
noBorder
|
noBorder
|
||||||
user={user}
|
user={user}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
@ -289,6 +300,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
position="left"
|
position="left"
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
customButton
|
customButton
|
||||||
user={user}
|
user={user}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
@ -301,6 +313,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
position="left"
|
position="left"
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
customButton
|
customButton
|
||||||
user={user}
|
user={user}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
@ -313,6 +326,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
<ViewDueDateSelect
|
<ViewDueDateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
noBorder
|
noBorder
|
||||||
user={user}
|
user={user}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
@ -325,6 +339,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
position="left"
|
position="left"
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
user={user}
|
user={user}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
|
@ -10,6 +10,7 @@ import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
|||||||
type Props = {
|
type Props = {
|
||||||
key: string;
|
key: string;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
|
index: number;
|
||||||
expandedIssues: string[];
|
expandedIssues: string[];
|
||||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
@ -24,6 +25,7 @@ type Props = {
|
|||||||
|
|
||||||
export const SpreadsheetIssues: React.FC<Props> = ({
|
export const SpreadsheetIssues: React.FC<Props> = ({
|
||||||
key,
|
key,
|
||||||
|
index,
|
||||||
issue,
|
issue,
|
||||||
expandedIssues,
|
expandedIssues,
|
||||||
setExpandedIssues,
|
setExpandedIssues,
|
||||||
@ -57,6 +59,7 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
<div>
|
<div>
|
||||||
<SingleSpreadsheetIssue
|
<SingleSpreadsheetIssue
|
||||||
issue={issue}
|
issue={issue}
|
||||||
|
index={index}
|
||||||
expanded={isExpanded}
|
expanded={isExpanded}
|
||||||
handleToggleExpand={handleToggleExpand}
|
handleToggleExpand={handleToggleExpand}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
gridTemplateColumns={gridTemplateColumns}
|
||||||
@ -77,6 +80,7 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
<SpreadsheetIssues
|
<SpreadsheetIssues
|
||||||
key={subIssue.id}
|
key={subIssue.id}
|
||||||
issue={subIssue}
|
issue={subIssue}
|
||||||
|
index={index}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
gridTemplateColumns={gridTemplateColumns}
|
||||||
|
@ -70,6 +70,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||||
<SpreadsheetIssues
|
<SpreadsheetIssues
|
||||||
key={`${issue.id}_${index}`}
|
key={`${issue.id}_${index}`}
|
||||||
|
index={index}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
|
@ -35,7 +35,7 @@ import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helpe
|
|||||||
import {
|
import {
|
||||||
isDateGreaterThanToday,
|
isDateGreaterThanToday,
|
||||||
renderDateFormat,
|
renderDateFormat,
|
||||||
renderShortDate,
|
renderShortDateWithYearFormat,
|
||||||
} from "helpers/date-time.helper";
|
} from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, ICycle } from "types";
|
import { ICurrentUserResponse, ICycle } from "types";
|
||||||
@ -315,7 +315,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<CalendarDaysIcon className="h-3 w-3" />
|
<CalendarDaysIcon className="h-3 w-3" />
|
||||||
<span>
|
<span>
|
||||||
{renderShortDate(
|
{renderShortDateWithYearFormat(
|
||||||
new Date(
|
new Date(
|
||||||
`${watch("start_date") ? watch("start_date") : cycle?.start_date}`
|
`${watch("start_date") ? watch("start_date") : cycle?.start_date}`
|
||||||
),
|
),
|
||||||
@ -366,7 +366,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
<CalendarDaysIcon className="h-3 w-3" />
|
<CalendarDaysIcon className="h-3 w-3" />
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{renderShortDate(
|
{renderShortDateWithYearFormat(
|
||||||
new Date(
|
new Date(
|
||||||
`${watch("end_date") ? watch("end_date") : cycle?.end_date}`
|
`${watch("end_date") ? watch("end_date") : cycle?.end_date}`
|
||||||
),
|
),
|
||||||
@ -408,7 +408,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
<div className="flex w-full flex-col gap-6 px-6 py-6">
|
<div className="flex w-full flex-col gap-6 px-6 py-6">
|
||||||
<div className="flex w-full flex-col items-start justify-start gap-2">
|
<div className="flex w-full flex-col items-start justify-start gap-2">
|
||||||
<div className="flex w-full items-start justify-between gap-2">
|
<div className="flex w-full items-start justify-between gap-2">
|
||||||
<h4 className="text-xl font-semibold text-brand-base">{cycle.name}</h4>
|
<div className="max-w-[300px]">
|
||||||
|
<h4 className="text-xl font-semibold text-brand-base break-words w-full">
|
||||||
|
{cycle.name}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
<CustomMenu width="lg" ellipsis>
|
<CustomMenu width="lg" ellipsis>
|
||||||
{!isCompleted && (
|
{!isCompleted && (
|
||||||
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
|
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
|
||||||
@ -427,7 +431,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="whitespace-normal text-sm leading-5 text-brand-secondary">
|
<span className="whitespace-normal text-sm leading-5 text-brand-secondary break-words w-full">
|
||||||
{cycle.description}
|
{cycle.description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -172,17 +172,19 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div className="max-w-2xl">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={cycle.name}
|
tooltipContent={cycle.name}
|
||||||
className="break-words"
|
className="break-words"
|
||||||
position="top-left"
|
position="top-left"
|
||||||
>
|
>
|
||||||
<h3 className="break-words text-base font-semibold">
|
<h3 className="break-words w-full text-base font-semibold">
|
||||||
{truncateText(cycle.name, 70)}
|
{truncateText(cycle.name, 70)}
|
||||||
</h3>
|
</h3>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<p className="mt-2 text-brand-secondary">{cycle.description}</p>
|
<p className="mt-2 text-brand-secondary break-words w-full">
|
||||||
|
{cycle.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-4 capitalize">
|
<span className="flex items-center gap-4 capitalize">
|
||||||
|
@ -18,7 +18,7 @@ export const GanttChartBlocks: FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative z-10 mt-[58px] h-full w-[4000px] divide-x divide-gray-300 overflow-hidden overflow-y-auto"
|
className="relative z-[5] mt-[58px] h-full w-[4000px] divide-x divide-gray-300 overflow-hidden overflow-y-auto"
|
||||||
style={{ width: `${itemsContainerWidth}px` }}
|
style={{ width: `${itemsContainerWidth}px` }}
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
26
apps/app/components/icons/calendar-after-icon.tsx
Normal file
26
apps/app/components/icons/calendar-after-icon.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import type { Props } from "./types";
|
||||||
|
|
||||||
|
export const CalendarAfterIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={className}
|
||||||
|
viewBox="0 0 18 19"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_3309_70901)">
|
||||||
|
<path
|
||||||
|
d="M10.6125 17V15.875H14.625V7.8125H3.375V11.9375H2.25V4.25C2.25 3.95 2.3625 3.6875 2.5875 3.4625C2.8125 3.2375 3.075 3.125 3.375 3.125H4.59375V2H5.8125V3.125H12.1875V2H13.4063V3.125H14.625C14.925 3.125 15.1875 3.2375 15.4125 3.4625C15.6375 3.6875 15.75 3.95 15.75 4.25V15.875C15.75 16.175 15.6375 16.4375 15.4125 16.6625C15.1875 16.8875 14.925 17 14.625 17H10.6125ZM6 18.2375L5.2125 17.45L7.33125 15.3125H0.9375V14.1875H7.33125L5.2125 12.05L6 11.2625L9.4875 14.75L6 18.2375ZM3.375 6.6875H14.625V4.25H3.375V6.6875Z"
|
||||||
|
fill="#858E96"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_3309_70901">
|
||||||
|
<rect width="18" height="18" fill="white" transform="translate(0 0.5)" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
37
apps/app/components/icons/calendar-before-icon.tsx
Normal file
37
apps/app/components/icons/calendar-before-icon.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import type { Props } from "./types";
|
||||||
|
|
||||||
|
export const CalendarBeforeIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={className}
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_3309_70907)">
|
||||||
|
<path
|
||||||
|
d="M10.6125 16.5V15.375H14.625V7.3125H3.375V11.4375H2.25V3.75C2.25 3.45 2.3625 3.1875 2.5875 2.9625C2.8125 2.7375 3.075 2.625 3.375 2.625H4.59375V1.5H5.8125V2.625H12.1875V1.5H13.4062V2.625H14.625C14.925 2.625 15.1875 2.7375 15.4125 2.9625C15.6375 3.1875 15.75 3.45 15.75 3.75V15.375C15.75 15.675 15.6375 15.9375 15.4125 16.1625C15.1875 16.3875 14.925 16.5 14.625 16.5H10.6125ZM3.375 6.1875H14.625V3.75H3.375V6.1875Z"
|
||||||
|
fill="#858E96"
|
||||||
|
/>
|
||||||
|
<g clipPath="url(#clip1_3309_70907)">
|
||||||
|
<path
|
||||||
|
d="M3.99967 17.1667L1.33301 14.5L3.99967 11.8334L4.34967 12.1834L2.28301 14.25H8V14.75H2.28301L4.34967 16.8167L3.99967 17.1667Z"
|
||||||
|
fill="#858E96"
|
||||||
|
stroke="#858E96"
|
||||||
|
strokeWidth="0.5"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_3309_70907">
|
||||||
|
<rect width="18" height="18" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_3309_70907">
|
||||||
|
<rect width="8" height="8" fill="white" transform="translate(0 10.5)" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
@ -3,17 +3,17 @@ import React from "react";
|
|||||||
import type { Props } from "./types";
|
import type { Props } from "./types";
|
||||||
|
|
||||||
export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||||
<svg
|
<svg
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
className={className}
|
className={className}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M12 14C11.7167 14 11.4792 13.9042 11.2875 13.7125C11.0958 13.5208 11 13.2833 11 13C11 12.7167 11.0958 12.4792 11.2875 12.2875C11.4792 12.0958 11.7167 12 12 12C12.2833 12 12.5208 12.0958 12.7125 12.2875C12.9042 12.4792 13 12.7167 13 13C13 13.2833 12.9042 13.5208 12.7125 13.7125C12.5208 13.9042 12.2833 14 12 14ZM8 14C7.71667 14 7.47917 13.9042 7.2875 13.7125C7.09583 13.5208 7 13.2833 7 13C7 12.7167 7.09583 12.4792 7.2875 12.2875C7.47917 12.0958 7.71667 12 8 12C8.28333 12 8.52083 12.0958 8.7125 12.2875C8.90417 12.4792 9 12.7167 9 13C9 13.2833 8.90417 13.5208 8.7125 13.7125C8.52083 13.9042 8.28333 14 8 14ZM16 14C15.7167 14 15.4792 13.9042 15.2875 13.7125C15.0958 13.5208 15 13.2833 15 13C15 12.7167 15.0958 12.4792 15.2875 12.2875C15.4792 12.0958 15.7167 12 16 12C16.2833 12 16.5208 12.0958 16.7125 12.2875C16.9042 12.4792 17 12.7167 17 13C17 13.2833 16.9042 13.5208 16.7125 13.7125C16.5208 13.9042 16.2833 14 16 14ZM12 18C11.7167 18 11.4792 17.9042 11.2875 17.7125C11.0958 17.5208 11 17.2833 11 17C11 16.7167 11.0958 16.4792 11.2875 16.2875C11.4792 16.0958 11.7167 16 12 16C12.2833 16 12.5208 16.0958 12.7125 16.2875C12.9042 16.4792 13 16.7167 13 17C13 17.2833 12.9042 17.5208 12.7125 17.7125C12.5208 17.9042 12.2833 18 12 18ZM8 18C7.71667 18 7.47917 17.9042 7.2875 17.7125C7.09583 17.5208 7 17.2833 7 17C7 16.7167 7.09583 16.4792 7.2875 16.2875C7.47917 16.0958 7.71667 16 8 16C8.28333 16 8.52083 16.0958 8.7125 16.2875C8.90417 16.4792 9 16.7167 9 17C9 17.2833 8.90417 17.5208 8.7125 17.7125C8.52083 17.9042 8.28333 18 8 18ZM16 18C15.7167 18 15.4792 17.9042 15.2875 17.7125C15.0958 17.5208 15 17.2833 15 17C15 16.7167 15.0958 16.4792 15.2875 16.2875C15.4792 16.0958 15.7167 16 16 16C16.2833 16 16.5208 16.0958 16.7125 16.2875C16.9042 16.4792 17 16.7167 17 17C17 17.2833 16.9042 17.5208 16.7125 17.7125C16.5208 17.9042 16.2833 18 16 18ZM4.5 22C4.1 22 3.75 21.85 3.45 21.55C3.15 21.25 3 20.9 3 20.5V5C3 4.6 3.15 4.25 3.45 3.95C3.75 3.65 4.1 3.5 4.5 3.5H6.125V2.8C6.125 2.56667 6.2 2.375 6.35 2.225C6.5 2.075 6.69167 2 6.925 2C7.15833 2 7.35417 2.075 7.5125 2.225C7.67083 2.375 7.75 2.56667 7.75 2.8V3.5H16.25V2.8C16.25 2.56667 16.325 2.375 16.475 2.225C16.625 2.075 16.8167 2 17.05 2C17.2833 2 17.4792 2.075 17.6375 2.225C17.7958 2.375 17.875 2.56667 17.875 2.8V3.5H19.5C19.9 3.5 20.25 3.65 20.55 3.95C20.85 4.25 21 4.6 21 5V20.5C21 20.9 20.85 21.25 20.55 21.55C20.25 21.85 19.9 22 19.5 22H4.5ZM4.5 20.5H19.5V9.75H4.5V20.5ZM4.5 8.25H19.5V5H4.5V8.25ZM4.5 8.25V5V8.25Z"
|
d="M12 14C11.7167 14 11.4792 13.9042 11.2875 13.7125C11.0958 13.5208 11 13.2833 11 13C11 12.7167 11.0958 12.4792 11.2875 12.2875C11.4792 12.0958 11.7167 12 12 12C12.2833 12 12.5208 12.0958 12.7125 12.2875C12.9042 12.4792 13 12.7167 13 13C13 13.2833 12.9042 13.5208 12.7125 13.7125C12.5208 13.9042 12.2833 14 12 14ZM8 14C7.71667 14 7.47917 13.9042 7.2875 13.7125C7.09583 13.5208 7 13.2833 7 13C7 12.7167 7.09583 12.4792 7.2875 12.2875C7.47917 12.0958 7.71667 12 8 12C8.28333 12 8.52083 12.0958 8.7125 12.2875C8.90417 12.4792 9 12.7167 9 13C9 13.2833 8.90417 13.5208 8.7125 13.7125C8.52083 13.9042 8.28333 14 8 14ZM16 14C15.7167 14 15.4792 13.9042 15.2875 13.7125C15.0958 13.5208 15 13.2833 15 13C15 12.7167 15.0958 12.4792 15.2875 12.2875C15.4792 12.0958 15.7167 12 16 12C16.2833 12 16.5208 12.0958 16.7125 12.2875C16.9042 12.4792 17 12.7167 17 13C17 13.2833 16.9042 13.5208 16.7125 13.7125C16.5208 13.9042 16.2833 14 16 14ZM12 18C11.7167 18 11.4792 17.9042 11.2875 17.7125C11.0958 17.5208 11 17.2833 11 17C11 16.7167 11.0958 16.4792 11.2875 16.2875C11.4792 16.0958 11.7167 16 12 16C12.2833 16 12.5208 16.0958 12.7125 16.2875C12.9042 16.4792 13 16.7167 13 17C13 17.2833 12.9042 17.5208 12.7125 17.7125C12.5208 17.9042 12.2833 18 12 18ZM8 18C7.71667 18 7.47917 17.9042 7.2875 17.7125C7.09583 17.5208 7 17.2833 7 17C7 16.7167 7.09583 16.4792 7.2875 16.2875C7.47917 16.0958 7.71667 16 8 16C8.28333 16 8.52083 16.0958 8.7125 16.2875C8.90417 16.4792 9 16.7167 9 17C9 17.2833 8.90417 17.5208 8.7125 17.7125C8.52083 17.9042 8.28333 18 8 18ZM16 18C15.7167 18 15.4792 17.9042 15.2875 17.7125C15.0958 17.5208 15 17.2833 15 17C15 16.7167 15.0958 16.4792 15.2875 16.2875C15.4792 16.0958 15.7167 16 16 16C16.2833 16 16.5208 16.0958 16.7125 16.2875C16.9042 16.4792 17 16.7167 17 17C17 17.2833 16.9042 17.5208 16.7125 17.7125C16.5208 17.9042 16.2833 18 16 18ZM4.5 22C4.1 22 3.75 21.85 3.45 21.55C3.15 21.25 3 20.9 3 20.5V5C3 4.6 3.15 4.25 3.45 3.95C3.75 3.65 4.1 3.5 4.5 3.5H6.125V2.8C6.125 2.56667 6.2 2.375 6.35 2.225C6.5 2.075 6.69167 2 6.925 2C7.15833 2 7.35417 2.075 7.5125 2.225C7.67083 2.375 7.75 2.56667 7.75 2.8V3.5H16.25V2.8C16.25 2.56667 16.325 2.375 16.475 2.225C16.625 2.075 16.8167 2 17.05 2C17.2833 2 17.4792 2.075 17.6375 2.225C17.7958 2.375 17.875 2.56667 17.875 2.8V3.5H19.5C19.9 3.5 20.25 3.65 20.55 3.95C20.85 4.25 21 4.6 21 5V20.5C21 20.9 20.85 21.25 20.55 21.55C20.25 21.85 19.9 22 19.5 22H4.5ZM4.5 20.5H19.5V9.75H4.5V20.5ZM4.5 8.25H19.5V5H4.5V8.25ZM4.5 8.25V5V8.25Z"
|
||||||
fill="#212529"
|
fill="#858E96"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,8 @@ export * from "./backlog-state-icon";
|
|||||||
export * from "./blocked-icon";
|
export * from "./blocked-icon";
|
||||||
export * from "./blocker-icon";
|
export * from "./blocker-icon";
|
||||||
export * from "./bolt-icon";
|
export * from "./bolt-icon";
|
||||||
|
export * from "./calendar-before-icon";
|
||||||
|
export * from "./calendar-after-icon";
|
||||||
export * from "./calendar-month-icon";
|
export * from "./calendar-month-icon";
|
||||||
export * from "./cancel-icon";
|
export * from "./cancel-icon";
|
||||||
export * from "./cancelled-state-icon";
|
export * from "./cancelled-state-icon";
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import type { IInboxIssue } from "types";
|
import type { IInboxIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
@ -72,12 +72,12 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipHeading="Created at"
|
tooltipHeading="Created on"
|
||||||
tooltipContent={`${renderShortNumericDateFormat(issue.created_at ?? "")}`}
|
tooltipContent={`${renderShortDateWithYearFormat(issue.created_at ?? "")}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1 rounded border border-brand-base shadow-sm text-xs px-2 py-[0.19rem] text-brand-secondary">
|
<div className="flex items-center gap-1 rounded border border-brand-base shadow-sm text-xs px-2 py-[0.19rem] text-brand-secondary">
|
||||||
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
||||||
<span>{renderShortNumericDateFormat(issue.created_at ?? "")}</span>
|
<span>{renderShortDateWithYearFormat(issue.created_at ?? "")}</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import type { IInboxIssue, IIssue } from "types";
|
import type { IInboxIssue, IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -252,13 +252,17 @@ export const InboxMainContent: React.FC = () => {
|
|||||||
{new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() ? (
|
{new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() ? (
|
||||||
<p>
|
<p>
|
||||||
This issue was snoozed till{" "}
|
This issue was snoozed till{" "}
|
||||||
{renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")}
|
{renderShortDateWithYearFormat(
|
||||||
|
issueDetails.issue_inbox[0].snoozed_till ?? ""
|
||||||
|
)}
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p>
|
<p>
|
||||||
This issue has been snoozed till{" "}
|
This issue has been snoozed till{" "}
|
||||||
{renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")}
|
{renderShortDateWithYearFormat(
|
||||||
|
issueDetails.issue_inbox[0].snoozed_till ?? ""
|
||||||
|
)}
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
|
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssueComment, IIssueLabels } from "types";
|
import { ICurrentUserResponse, IIssueComment, IIssueLabels } from "types";
|
||||||
@ -299,7 +299,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
|||||||
activityItem.new_value && activityItem.new_value !== ""
|
activityItem.new_value && activityItem.new_value !== ""
|
||||||
? activityItem.new_value
|
? activityItem.new_value
|
||||||
: activityItem.old_value;
|
: activityItem.old_value;
|
||||||
value = renderShortNumericDateFormat(date as string);
|
value = renderShortDateWithYearFormat(date as string);
|
||||||
} else if (activityItem.field === "description") {
|
} else if (activityItem.field === "description") {
|
||||||
value = "description";
|
value = "description";
|
||||||
} else if (activityItem.field === "attachment") {
|
} else if (activityItem.field === "attachment") {
|
||||||
|
@ -5,7 +5,7 @@ import { CalendarDaysIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
|||||||
// react-datepicker
|
// react-datepicker
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
// import "react-datepicker/dist/react-datepicker.css";
|
// import "react-datepicker/dist/react-datepicker.css";
|
||||||
import { renderDateFormat } from "helpers/date-time.helper";
|
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
@ -20,7 +20,7 @@ export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
|
|||||||
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-brand-secondary">
|
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-brand-secondary">
|
||||||
{value ? (
|
{value ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-brand-base">{value}</span>
|
<span className="text-brand-base">{renderShortDateWithYearFormat(value)}</span>
|
||||||
<button onClick={() => onChange(null)}>
|
<button onClick={() => onChange(null)}>
|
||||||
<XMarkIcon className="h-3 w-3" />
|
<XMarkIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -20,8 +20,8 @@ type Props = {
|
|||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
|
tooltipPosition?: "top" | "bottom";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
tooltipPosition?: "left" | "right";
|
|
||||||
customButton?: boolean;
|
customButton?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
@ -32,7 +32,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
position = "left",
|
position = "left",
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
tooltipPosition = "right",
|
tooltipPosition = "top",
|
||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
customButton = false,
|
customButton = false,
|
||||||
@ -69,7 +69,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
|
|
||||||
const assigneeLabel = (
|
const assigneeLabel = (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position={`top-${tooltipPosition}`}
|
position={tooltipPosition}
|
||||||
tooltipHeading="Assignees"
|
tooltipHeading="Assignees"
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
issue.assignee_details.length > 0
|
issue.assignee_details.length > 0
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker, Tooltip } from "components/ui";
|
import { CustomDatePicker, Tooltip } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { findHowManyDaysLeft } from "helpers/date-time.helper";
|
import { findHowManyDaysLeft, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// services
|
// services
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
// types
|
// types
|
||||||
@ -12,6 +12,7 @@ import { ICurrentUserResponse, IIssue } from "types";
|
|||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
tooltipPosition?: "top" | "bottom";
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
@ -20,6 +21,7 @@ type Props = {
|
|||||||
export const ViewDueDateSelect: React.FC<Props> = ({
|
export const ViewDueDateSelect: React.FC<Props> = ({
|
||||||
issue,
|
issue,
|
||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
|
tooltipPosition = "top",
|
||||||
noBorder = false,
|
noBorder = false,
|
||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
@ -28,7 +30,13 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
|||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading="Due Date" tooltipContent={issue.target_date ?? "N/A"}>
|
<Tooltip
|
||||||
|
tooltipHeading="Due Date"
|
||||||
|
tooltipContent={
|
||||||
|
issue.target_date ? renderShortDateWithYearFormat(issue.target_date) ?? "N/A" : "N/A"
|
||||||
|
}
|
||||||
|
position={tooltipPosition}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`group relative max-w-[6.5rem] ${
|
className={`group relative max-w-[6.5rem] ${
|
||||||
issue.target_date === null
|
issue.target_date === null
|
||||||
|
@ -17,6 +17,7 @@ type Props = {
|
|||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
|
tooltipPosition?: "top" | "bottom";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
customButton?: boolean;
|
customButton?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
@ -27,6 +28,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
|||||||
issue,
|
issue,
|
||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
position = "left",
|
position = "left",
|
||||||
|
tooltipPosition = "top",
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
customButton = false,
|
customButton = false,
|
||||||
user,
|
user,
|
||||||
@ -40,7 +42,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
|||||||
const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
|
const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
|
||||||
|
|
||||||
const estimateLabels = (
|
const estimateLabels = (
|
||||||
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}>
|
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-brand-secondary">
|
||||||
<PlayIcon className="h-3.5 w-3.5 -rotate-90" />
|
<PlayIcon className="h-3.5 w-3.5 -rotate-90" />
|
||||||
{estimateValue ?? "None"}
|
{estimateValue ?? "None"}
|
||||||
|
@ -22,7 +22,7 @@ type Props = {
|
|||||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
tooltipPosition?: "left" | "right";
|
tooltipPosition?: "top" | "bottom";
|
||||||
customButton?: boolean;
|
customButton?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
@ -33,7 +33,7 @@ export const ViewLabelSelect: React.FC<Props> = ({
|
|||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
position = "left",
|
position = "left",
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
tooltipPosition = "right",
|
tooltipPosition = "top",
|
||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
customButton = false,
|
customButton = false,
|
||||||
@ -68,7 +68,7 @@ export const ViewLabelSelect: React.FC<Props> = ({
|
|||||||
|
|
||||||
const labelsLabel = (
|
const labelsLabel = (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position={`top-${tooltipPosition}`}
|
position={tooltipPosition}
|
||||||
tooltipHeading="Labels"
|
tooltipHeading="Labels"
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
issue.label_details.length > 0
|
issue.label_details.length > 0
|
||||||
@ -118,6 +118,8 @@ export const ViewLabelSelect: React.FC<Props> = ({
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const noResultIcon = <TagIcon className="h-3.5 w-3.5 text-brand-secondary" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{projectId && (
|
{projectId && (
|
||||||
@ -141,6 +143,7 @@ export const ViewLabelSelect: React.FC<Props> = ({
|
|||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
selfPositioned={selfPositioned}
|
selfPositioned={selfPositioned}
|
||||||
footerOption={footerOption}
|
footerOption={footerOption}
|
||||||
|
noResultIcon={noResultIcon}
|
||||||
dropdownWidth="w-full min-w-[12rem]"
|
dropdownWidth="w-full min-w-[12rem]"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -19,6 +19,7 @@ type Props = {
|
|||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
|
tooltipPosition?: "top" | "bottom";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
@ -29,6 +30,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
issue,
|
issue,
|
||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
position = "left",
|
position = "left",
|
||||||
|
tooltipPosition = "top",
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
noBorder = false,
|
noBorder = false,
|
||||||
user,
|
user,
|
||||||
@ -75,7 +77,11 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
: "border-brand-base"
|
: "border-brand-base"
|
||||||
} items-center`}
|
} items-center`}
|
||||||
>
|
>
|
||||||
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
|
<Tooltip
|
||||||
|
tooltipHeading="Priority"
|
||||||
|
tooltipContent={issue.priority ?? "None"}
|
||||||
|
position={tooltipPosition}
|
||||||
|
>
|
||||||
<span className="flex gap-1 items-center text-brand-secondary text-xs">
|
<span className="flex gap-1 items-center text-brand-secondary text-xs">
|
||||||
{getPriorityIcon(
|
{getPriorityIcon(
|
||||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||||
|
@ -21,6 +21,7 @@ type Props = {
|
|||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
|
tooltipPosition?: "top" | "bottom";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
customButton?: boolean;
|
customButton?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
@ -31,6 +32,7 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
issue,
|
issue,
|
||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
position = "left",
|
position = "left",
|
||||||
|
tooltipPosition = "top",
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
customButton = false,
|
customButton = false,
|
||||||
user,
|
user,
|
||||||
@ -64,6 +66,7 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipHeading="State"
|
tooltipHeading="State"
|
||||||
tooltipContent={addSpaceIfCamelCase(selectedOption?.name ?? "")}
|
tooltipContent={addSpaceIfCamelCase(selectedOption?.name ?? "")}
|
||||||
|
position={tooltipPosition}
|
||||||
>
|
>
|
||||||
<div className="flex items-center cursor-pointer gap-2 text-brand-secondary">
|
<div className="flex items-center cursor-pointer gap-2 text-brand-secondary">
|
||||||
{selectedOption &&
|
{selectedOption &&
|
||||||
|
@ -34,7 +34,7 @@ import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui";
|
|||||||
import { ExclamationIcon } from "components/icons";
|
import { ExclamationIcon } from "components/icons";
|
||||||
import { LinkIcon } from "@heroicons/react/20/solid";
|
import { LinkIcon } from "@heroicons/react/20/solid";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderDateFormat, renderShortDate } from "helpers/date-time.helper";
|
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
|
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, IModule, ModuleLink } from "types";
|
import { ICurrentUserResponse, IIssue, IModule, ModuleLink } from "types";
|
||||||
@ -228,7 +228,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
>
|
>
|
||||||
<CalendarDaysIcon className="h-3 w-3" />
|
<CalendarDaysIcon className="h-3 w-3" />
|
||||||
<span>
|
<span>
|
||||||
{renderShortDate(new Date(`${module.start_date}`), "Start date")}
|
{renderShortDateWithYearFormat(
|
||||||
|
new Date(`${module.start_date}`),
|
||||||
|
"Start date"
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
@ -279,7 +282,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
<CalendarDaysIcon className="h-3 w-3 " />
|
<CalendarDaysIcon className="h-3 w-3 " />
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{renderShortDate(new Date(`${module?.target_date}`), "End date")}
|
{renderShortDateWithYearFormat(
|
||||||
|
new Date(`${module?.target_date}`),
|
||||||
|
"End date"
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
@ -322,7 +328,11 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
<div className="flex w-full flex-col gap-6 px-6 py-6">
|
<div className="flex w-full flex-col gap-6 px-6 py-6">
|
||||||
<div className="flex w-full flex-col items-start justify-start gap-2">
|
<div className="flex w-full flex-col items-start justify-start gap-2">
|
||||||
<div className="flex w-full items-start justify-between gap-2 ">
|
<div className="flex w-full items-start justify-between gap-2 ">
|
||||||
<h4 className="text-xl font-semibold text-brand-base">{module.name}</h4>
|
<div className="max-w-[300px]">
|
||||||
|
<h4 className="text-xl font-semibold break-words w-full text-brand-base">
|
||||||
|
{module.name}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
<CustomMenu width="lg" ellipsis>
|
<CustomMenu width="lg" ellipsis>
|
||||||
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
|
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
@ -339,7 +349,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="whitespace-normal text-sm leading-5 text-brand-secondary">
|
<span className="whitespace-normal text-sm leading-5 text-brand-secondary break-words w-full">
|
||||||
{module.description}
|
{module.description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,7 +41,7 @@ export const RecentPagesList: React.FC<TPagesListProps> = ({ viewType }) => {
|
|||||||
if (pages[key].length === 0) return null;
|
if (pages[key].length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key}>
|
<div key={key} className="h-full overflow-hidden">
|
||||||
<h2 className="text-xl font-semibold capitalize mb-2">
|
<h2 className="text-xl font-semibold capitalize mb-2">
|
||||||
{replaceUnderscoreIfSnakeCase(key)}
|
{replaceUnderscoreIfSnakeCase(key)}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import type { IFavoriteProject, IProject } from "types";
|
import type { IFavoriteProject, IProject } from "types";
|
||||||
@ -202,13 +202,13 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
|||||||
</Link>
|
</Link>
|
||||||
<div className="flex h-full items-end justify-between">
|
<div className="flex h-full items-end justify-between">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={`Created at ${renderShortNumericDateFormat(project.created_at)}`}
|
tooltipContent={`Created at ${renderShortDateWithYearFormat(project.created_at)}`}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
>
|
>
|
||||||
<div className="flex cursor-default items-center gap-1.5 text-xs">
|
<div className="flex cursor-default items-center gap-1.5 text-xs">
|
||||||
<CalendarDaysIcon className="h-4 w-4" />
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
{renderShortNumericDateFormat(project.created_at)}
|
{renderShortDateWithYearFormat(project.created_at)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{hasJoined ? (
|
{hasJoined ? (
|
||||||
|
@ -29,6 +29,7 @@ type CustomSearchSelectProps = {
|
|||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
footerOption?: JSX.Element;
|
footerOption?: JSX.Element;
|
||||||
|
noResultIcon?: JSX.Element;
|
||||||
dropdownWidth?: string;
|
dropdownWidth?: string;
|
||||||
};
|
};
|
||||||
export const CustomSearchSelect = ({
|
export const CustomSearchSelect = ({
|
||||||
@ -47,6 +48,7 @@ export const CustomSearchSelect = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
multiple = false,
|
multiple = false,
|
||||||
|
noResultIcon,
|
||||||
footerOption,
|
footerOption,
|
||||||
dropdownWidth,
|
dropdownWidth,
|
||||||
}: CustomSearchSelectProps) => {
|
}: CustomSearchSelectProps) => {
|
||||||
@ -171,7 +173,10 @@ export const CustomSearchSelect = ({
|
|||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-brand-secondary">No matching results</p>
|
<span className="flex items-center gap-2 p-1">
|
||||||
|
{noResultIcon && noResultIcon}
|
||||||
|
<p className="text-left text-brand-secondary ">No matching results</p>
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-brand-secondary">Loading...</p>
|
<p className="text-center text-brand-secondary">Loading...</p>
|
||||||
|
@ -5,7 +5,7 @@ import { CalendarDaysIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
|||||||
// react-datepicker
|
// react-datepicker
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
// import "react-datepicker/dist/react-datepicker.css";
|
// import "react-datepicker/dist/react-datepicker.css";
|
||||||
import { renderDateFormat } from "helpers/date-time.helper";
|
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
@ -21,7 +21,7 @@ export const DateSelect: React.FC<Props> = ({ value, onChange, label }) => (
|
|||||||
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-brand-secondary">
|
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-brand-secondary">
|
||||||
{value ? (
|
{value ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-brand-base">{value}</span>
|
<span className="text-brand-base">{renderShortDateWithYearFormat(value)}</span>
|
||||||
<button onClick={() => onChange(null)}>
|
<button onClick={() => onChange(null)}>
|
||||||
<XMarkIcon className="h-3 w-3" />
|
<XMarkIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -38,9 +38,9 @@ export const CustomDatePicker: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
className={`${
|
className={`${
|
||||||
renderAs === "input"
|
renderAs === "input"
|
||||||
? "block px-3 py-2 text-sm focus:outline-none"
|
? "block px-2 py-2 text-sm focus:outline-none"
|
||||||
: renderAs === "button"
|
: renderAs === "button"
|
||||||
? `px-3 py-1 text-xs shadow-sm ${
|
? `px-2 py-1 text-xs shadow-sm ${
|
||||||
disabled ? "" : "hover:bg-brand-surface-2"
|
disabled ? "" : "hover:bg-brand-surface-2"
|
||||||
} duration-300 focus:border-brand-accent focus:outline-none focus:ring-1 focus:ring-brand-accent`
|
} 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<Props> = ({
|
|||||||
} ${
|
} ${
|
||||||
noBorder ? "" : "border border-brand-base"
|
noBorder ? "" : "border border-brand-base"
|
||||||
} w-full rounded-md bg-transparent caret-transparent ${className}`}
|
} w-full rounded-md bg-transparent caret-transparent ${className}`}
|
||||||
dateFormat="dd-MM-yyyy"
|
dateFormat="MMM dd, yyyy"
|
||||||
isClearable={isClearable}
|
isClearable={isClearable}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
@ -18,6 +18,7 @@ type MultiLevelDropdownProps = {
|
|||||||
label: string | JSX.Element;
|
label: string | JSX.Element;
|
||||||
value: any;
|
value: any;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
element?: JSX.Element;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
onSelect: (value: any) => void;
|
onSelect: (value: any) => void;
|
||||||
@ -35,117 +36,121 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|||||||
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
<>
|
||||||
{({ open }) => (
|
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||||
<>
|
{({ open }) => (
|
||||||
<div>
|
<>
|
||||||
<Menu.Button
|
<div>
|
||||||
onClick={() => setOpenChildFor(null)}
|
<Menu.Button
|
||||||
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 ${
|
onClick={() => setOpenChildFor(null)}
|
||||||
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
|
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}
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
{label}
|
<Menu.Items
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
static
|
||||||
</Menu.Button>
|
className="absolute right-0 z-10 mt-1 w-36 origin-top-right select-none rounded-md bg-brand-surface-1 text-xs shadow-lg focus:outline-none"
|
||||||
</div>
|
>
|
||||||
<Transition
|
{options.map((option) => (
|
||||||
as={Fragment}
|
<div className="relative p-1" key={option.id}>
|
||||||
enter="transition ease-out duration-100"
|
<Menu.Item
|
||||||
enterFrom="transform opacity-0 scale-95"
|
as="button"
|
||||||
enterTo="transform opacity-100 scale-100"
|
onClick={(e: any) => {
|
||||||
leave="transition ease-in duration-75"
|
if (option.children) {
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
e.stopPropagation();
|
||||||
leaveTo="transform opacity-0 scale-95"
|
e.preventDefault();
|
||||||
>
|
|
||||||
<Menu.Items
|
|
||||||
static
|
|
||||||
className="absolute right-0 z-10 mt-1 w-36 origin-top-right select-none rounded-md bg-brand-surface-1 text-xs shadow-lg focus:outline-none"
|
|
||||||
>
|
|
||||||
{options.map((option) => (
|
|
||||||
<div className="relative p-1" key={option.id}>
|
|
||||||
<Menu.Item
|
|
||||||
as="button"
|
|
||||||
onClick={(e: any) => {
|
|
||||||
if (option.children) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (openChildFor === option.id) setOpenChildFor(null);
|
if (openChildFor === option.id) setOpenChildFor(null);
|
||||||
else setOpenChildFor(option.id);
|
else setOpenChildFor(option.id);
|
||||||
} else {
|
} else {
|
||||||
onSelect(option.value);
|
onSelect(option.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
|
||||||
{({ active }) => (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
active || option.selected ? "bg-brand-surface-2" : ""
|
|
||||||
} flex items-center gap-1 rounded px-1 py-1.5 text-brand-secondary ${
|
|
||||||
direction === "right" ? "justify-between" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{direction === "left" && option.children && (
|
|
||||||
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
<span>{option.label}</span>
|
|
||||||
{direction === "right" && option.children && (
|
|
||||||
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
{option.children && option.id === openChildFor && (
|
|
||||||
<div
|
|
||||||
className={`absolute top-0 w-auto min-w-[144px] max-w-[192px] origin-top-right select-none overflow-y-scroll rounded-md bg-brand-surface-1 shadow-lg focus:outline-none ${
|
|
||||||
direction === "left"
|
|
||||||
? "right-full -translate-x-1"
|
|
||||||
: "left-full translate-x-1"
|
|
||||||
} ${
|
|
||||||
height === "sm"
|
|
||||||
? "max-h-28"
|
|
||||||
: height === "md"
|
|
||||||
? "max-h-44"
|
|
||||||
: height === "rg"
|
|
||||||
? "max-h-56"
|
|
||||||
: height === "lg"
|
|
||||||
? "max-h-80"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="space-y-1 p-1">
|
{({ active }) => (
|
||||||
{option.children.map((child) => (
|
<>
|
||||||
<button
|
<div
|
||||||
key={child.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
onSelect(child.value);
|
|
||||||
}}
|
|
||||||
className={`${
|
className={`${
|
||||||
child.selected ? "bg-brand-surface-2" : ""
|
active || option.selected ? "bg-brand-surface-2" : ""
|
||||||
} flex w-full items-center justify-between whitespace-nowrap break-words rounded px-1 py-1.5 text-left capitalize text-brand-secondary hover:bg-brand-surface-2`}
|
} flex items-center gap-1 rounded px-1 py-1.5 text-brand-secondary ${
|
||||||
|
direction === "right" ? "justify-between" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{child.label}
|
{direction === "left" && option.children && (
|
||||||
<CheckIcon
|
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
className={`h-3.5 w-3.5 opacity-0 ${
|
)}
|
||||||
child.selected ? "opacity-100" : ""
|
<span>{option.label}</span>
|
||||||
}`}
|
{direction === "right" && option.children && (
|
||||||
/>
|
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
</button>
|
)}
|
||||||
))}
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
{option.children && option.id === openChildFor && (
|
||||||
|
<div
|
||||||
|
className={`absolute top-0 w-36 origin-top-right select-none overflow-y-scroll rounded-md bg-brand-surface-1 shadow-lg focus:outline-none ${
|
||||||
|
direction === "left"
|
||||||
|
? "right-full -translate-x-1"
|
||||||
|
: "left-full translate-x-1"
|
||||||
|
} ${
|
||||||
|
height === "sm"
|
||||||
|
? "max-h-28"
|
||||||
|
: height === "md"
|
||||||
|
? "max-h-44"
|
||||||
|
: height === "rg"
|
||||||
|
? "max-h-56"
|
||||||
|
: height === "lg"
|
||||||
|
? "max-h-80"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="space-y-1 p-1">
|
||||||
|
{option.children.map((child) => {
|
||||||
|
if (child.element) return child.element;
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={child.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelect(child.value)}
|
||||||
|
className={`${
|
||||||
|
child.selected ? "bg-brand-surface-2" : ""
|
||||||
|
} flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2`}
|
||||||
|
>
|
||||||
|
{child.label}{" "}
|
||||||
|
<CheckIcon
|
||||||
|
className={`h-3.5 w-3.5 opacity-0 ${
|
||||||
|
child.selected ? "opacity-100" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</Menu.Items>
|
||||||
</Menu.Items>
|
</Transition>
|
||||||
</Transition>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</Menu>
|
||||||
</Menu>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -6,18 +8,22 @@ import useSWR from "swr";
|
|||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
|
// components
|
||||||
|
import { DueDateFilterModal } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, MultiLevelDropdown } from "components/ui";
|
import { Avatar, MultiLevelDropdown } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { getStatesList } from "helpers/state.helper";
|
import { getStatesList } from "helpers/state.helper";
|
||||||
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions, IQuery } from "types";
|
import { IIssueFilterOptions, IQuery } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { PRIORITIES } from "constants/project";
|
import { PRIORITIES } from "constants/project";
|
||||||
|
import { DUE_DATES } from "constants/due-dates";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filters: Partial<IIssueFilterOptions> | IQuery;
|
filters: Partial<IIssueFilterOptions> | IQuery;
|
||||||
@ -32,6 +38,8 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
direction = "right",
|
direction = "right",
|
||||||
height = "md",
|
height = "md",
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -58,125 +66,163 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiLevelDropdown
|
<>
|
||||||
label="Filters"
|
{isDueDateFilterModalOpen && (
|
||||||
onSelect={onSelect}
|
<DueDateFilterModal
|
||||||
direction={direction}
|
isOpen={isDueDateFilterModalOpen}
|
||||||
height={height}
|
handleClose={() => setIsDueDateFilterModalOpen(false)}
|
||||||
options={[
|
/>
|
||||||
{
|
)}
|
||||||
id: "priority",
|
<MultiLevelDropdown
|
||||||
label: "Priority",
|
label="Filters"
|
||||||
value: PRIORITIES,
|
onSelect={onSelect}
|
||||||
children: [
|
direction={direction}
|
||||||
...PRIORITIES.map((priority) => ({
|
height={height}
|
||||||
id: priority === null ? "null" : priority,
|
options={[
|
||||||
label: (
|
{
|
||||||
<div className="flex items-center gap-2">
|
id: "priority",
|
||||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
label: "Priority",
|
||||||
</div>
|
value: PRIORITIES,
|
||||||
),
|
children: [
|
||||||
value: {
|
...PRIORITIES.map((priority) => ({
|
||||||
key: "priority",
|
id: priority === null ? "null" : priority,
|
||||||
value: priority === null ? "null" : priority,
|
label: (
|
||||||
|
<div className="flex items-center gap-2 capitalize">
|
||||||
|
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.first_name && member.member.first_name !== ""
|
||||||
|
? member.member.first_name
|
||||||
|
: member.member.email}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.first_name && member.member.first_name !== ""
|
||||||
|
? member.member.first_name
|
||||||
|
: member.member.email}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
label.color && label.color !== "" ? label.color : "#000000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{label.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsDueDateFilterModalOpen(true)}
|
||||||
|
className="w-full rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2"
|
||||||
|
>
|
||||||
|
Custom
|
||||||
|
</button>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
],
|
||||||
})),
|
},
|
||||||
],
|
]}
|
||||||
},
|
/>
|
||||||
{
|
</>
|
||||||
id: "state",
|
|
||||||
label: "State",
|
|
||||||
value: statesList,
|
|
||||||
children: [
|
|
||||||
...statesList.map((state) => ({
|
|
||||||
id: state.id,
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
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: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar user={member.member} />
|
|
||||||
{member.member.first_name && member.member.first_name !== ""
|
|
||||||
? member.member.first_name
|
|
||||||
: member.member.email}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
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: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar user={member.member} />
|
|
||||||
{member.member.first_name && member.member.first_name !== ""
|
|
||||||
? member.member.first_name
|
|
||||||
: member.member.email}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
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: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="h-2 w-2 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{label.name}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: {
|
|
||||||
key: "labels",
|
|
||||||
value: label.id,
|
|
||||||
},
|
|
||||||
selected: filters?.labels?.includes(label.id),
|
|
||||||
})) ?? []),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,7 @@ import { VIEWS_LIST } from "constants/fetch-keys";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat, renderShortTime } from "helpers/date-time.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
view: IView;
|
view: IView;
|
||||||
@ -107,7 +107,7 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={`Last updated at ${renderShortTime(
|
tooltipContent={`Last updated at ${renderShortTime(
|
||||||
view.updated_at
|
view.updated_at
|
||||||
)} ${renderShortDate(view.updated_at)}`}
|
)} ${renderShortDateWithYearFormat(view.updated_at)}`}
|
||||||
>
|
>
|
||||||
<p className="text-sm text-brand-secondary">
|
<p className="text-sm text-brand-secondary">
|
||||||
{renderShortTime(view.updated_at)}
|
{renderShortTime(view.updated_at)}
|
||||||
|
@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "components/ui";
|
import { Tooltip } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-time.helper";
|
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { IUserActivity } from "types";
|
import { IUserActivity } from "types";
|
||||||
// constants
|
// constants
|
||||||
@ -109,7 +109,7 @@ export const ActivityGraph: React.FC<Props> = ({ activities }) => {
|
|||||||
key={`${date}-${index}`}
|
key={`${date}-${index}`}
|
||||||
tooltipContent={`${
|
tooltipContent={`${
|
||||||
isActive ? isActive.activity_count : 0
|
isActive ? isActive.activity_count : 0
|
||||||
} activities on ${renderShortNumericDateFormat(date)}`}
|
} activities on ${renderShortDateWithYearFormat(date)}`}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -25,8 +25,12 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
|
|||||||
})) ?? []
|
})) ?? []
|
||||||
}
|
}
|
||||||
height="320px"
|
height="320px"
|
||||||
innerRadius={0.5}
|
innerRadius={0.6}
|
||||||
arcLinkLabel={(cell) => `${capitalizeFirstLetter(cell.label.toString())} (${cell.value})`}
|
cornerRadius={5}
|
||||||
|
padAngle={2}
|
||||||
|
enableArcLabels
|
||||||
|
arcLabelsTextColor="#000000"
|
||||||
|
enableArcLinkLabels={false}
|
||||||
legends={[
|
legends={[
|
||||||
{
|
{
|
||||||
anchor: "right",
|
anchor: "right",
|
||||||
@ -53,8 +57,14 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
|
|||||||
]}
|
]}
|
||||||
activeInnerRadiusOffset={5}
|
activeInnerRadiusOffset={5}
|
||||||
colors={(datum) => datum.data.color}
|
colors={(datum) => datum.data.color}
|
||||||
|
tooltip={(datum) => (
|
||||||
|
<div className="flex items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
|
||||||
|
<span className="text-brand-secondary capitalize">{datum.datum.label} issues:</span>{" "}
|
||||||
|
{datum.datum.value}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
theme={{
|
theme={{
|
||||||
background: "rgb(var(--color-bg-base))",
|
background: "transparent",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
37
apps/app/constants/due-dates.ts
Normal file
37
apps/app/constants/due-dates.ts
Normal file
@ -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`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||||
|
|
||||||
export const GROUP_CHOICES = {
|
export const GROUP_CHOICES = {
|
||||||
|
@ -89,6 +89,7 @@ export const initialState: StateType = {
|
|||||||
issue__assignees__id: null,
|
issue__assignees__id: null,
|
||||||
issue__labels__id: null,
|
issue__labels__id: null,
|
||||||
created_by: null,
|
created_by: null,
|
||||||
|
target_date: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,3 +42,11 @@ export const findStringWithMostCharacters = (strings: string[]) =>
|
|||||||
strings.reduce((longestString, currentString) =>
|
strings.reduce((longestString, currentString) =>
|
||||||
currentString.length > longestString.length ? currentString : longestString
|
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));
|
||||||
|
};
|
||||||
|
@ -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;
|
if (!date || date === "") return null;
|
||||||
|
|
||||||
date = new Date(date);
|
date = new Date(date);
|
||||||
@ -136,7 +136,8 @@ export const renderShortDateWithYearFormat = (date: string | Date) => {
|
|||||||
const day = date.getDate();
|
const day = date.getDate();
|
||||||
const month = months[date.getMonth()];
|
const month = months[date.getMonth()];
|
||||||
const year = date.getFullYear();
|
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) => {
|
export const renderShortDate = (date: string | Date, placeholder?: string) => {
|
||||||
|
@ -60,6 +60,7 @@ const useIssuesView = () => {
|
|||||||
? filters?.issue__labels__id.join(",")
|
? filters?.issue__labels__id.join(",")
|
||||||
: undefined,
|
: undefined,
|
||||||
created_by: filters?.created_by ? filters?.created_by.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(
|
const { data: projectIssues } = useSWR(
|
||||||
|
@ -159,7 +159,7 @@ const SingleCycle: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||||
<div
|
<div
|
||||||
className={`h-full ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
||||||
analyticsModal ? "mr-[50%]" : ""
|
analyticsModal ? "mr-[50%]" : ""
|
||||||
} duration-300`}
|
} duration-300`}
|
||||||
>
|
>
|
||||||
|
@ -120,10 +120,10 @@ const IssueDetailsPage: NextPage = () => {
|
|||||||
>
|
>
|
||||||
{issueDetails && projectId ? (
|
{issueDetails && projectId ? (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
<div className="basis-2/3 space-y-5 divide-y-2 divide-brand-base p-5">
|
<div className="w-2/3 space-y-5 divide-y-2 divide-brand-base p-5">
|
||||||
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />
|
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />
|
||||||
</div>
|
</div>
|
||||||
<div className="basis-1/3 space-y-5 border-l border-brand-base p-5">
|
<div className="w-1/3 space-y-5 border-l border-brand-base p-5">
|
||||||
<IssueDetailsSidebar
|
<IssueDetailsSidebar
|
||||||
control={control}
|
control={control}
|
||||||
issueDetail={issueDetails}
|
issueDetail={issueDetails}
|
||||||
|
@ -99,7 +99,9 @@ const ProjectIssues: NextPage = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||||
<IssuesView />
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<IssuesView />
|
||||||
|
</div>
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
</IssueViewContextProvider>
|
</IssueViewContextProvider>
|
||||||
);
|
);
|
||||||
|
@ -164,7 +164,7 @@ const SingleModule: React.FC = () => {
|
|||||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`h-full ${moduleSidebar ? "mr-[24rem]" : ""} ${
|
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${
|
||||||
analyticsModal ? "mr-[50%]" : ""
|
analyticsModal ? "mr-[50%]" : ""
|
||||||
} duration-300`}
|
} duration-300`}
|
||||||
>
|
>
|
||||||
|
@ -124,7 +124,29 @@ const ProjectPages: NextPage = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="space-y-5 p-8 h-full overflow-hidden flex flex-col">
|
<div className="space-y-5 p-8 h-full overflow-hidden flex flex-col">
|
||||||
<h3 className="text-2xl font-semibold text-brand-base">Pages</h3>
|
<div className="flex gap-4 justify-between">
|
||||||
|
<h3 className="text-2xl font-semibold text-brand-base">Pages</h3>
|
||||||
|
<div className="flex gap-x-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||||
|
viewType === "list" ? "bg-brand-surface-2" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setViewType("list")}
|
||||||
|
>
|
||||||
|
<ListBulletIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||||
|
viewType === "detailed" ? "bg-brand-surface-2" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setViewType("detailed")}
|
||||||
|
>
|
||||||
|
<Squares2X2Icon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Tab.Group
|
<Tab.Group
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
defaultIndex={currentTabValue(pageTab)}
|
defaultIndex={currentTabValue(pageTab)}
|
||||||
@ -147,7 +169,7 @@ const ProjectPages: NextPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab.List as="div" className="mb-6 flex items-center justify-between">
|
<Tab.List as="div" className="mb-6 flex items-center justify-between">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4 items-center flex-wrap">
|
||||||
{tabsList.map((tab, index) => (
|
{tabsList.map((tab, index) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={`${tab}-${index}`}
|
key={`${tab}-${index}`}
|
||||||
@ -163,26 +185,6 @@ const ProjectPages: NextPage = () => {
|
|||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-x-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
|
||||||
viewType === "list" ? "bg-brand-surface-2" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setViewType("list")}
|
|
||||||
>
|
|
||||||
<ListBulletIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
|
||||||
viewType === "detailed" ? "bg-brand-surface-2" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setViewType("detailed")}
|
|
||||||
>
|
|
||||||
<Squares2X2Icon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels as={Fragment}>
|
<Tab.Panels as={Fragment}>
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto space-y-5">
|
<Tab.Panel as="div" className="h-full overflow-y-auto space-y-5">
|
||||||
|
@ -101,7 +101,9 @@ const SingleView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IssuesView />
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<IssuesView />
|
||||||
|
</div>
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
</IssueViewContextProvider>
|
</IssueViewContextProvider>
|
||||||
);
|
);
|
||||||
|
@ -197,25 +197,6 @@ body {
|
|||||||
outline: none;
|
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 {
|
.conical-gradient {
|
||||||
background: conic-gradient(
|
background: conic-gradient(
|
||||||
from 180deg at 50% 50%,
|
from 180deg at 50% 50%,
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__day-name {
|
.react-datepicker__day-name {
|
||||||
color: rgba(var(--color-text-base)) !important;
|
color: rgba(var(--color-text-secondary)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__week {
|
.react-datepicker__week {
|
||||||
|
1
apps/app/types/issues.d.ts
vendored
1
apps/app/types/issues.d.ts
vendored
@ -239,6 +239,7 @@ export interface IIssueLite {
|
|||||||
export interface IIssueFilterOptions {
|
export interface IIssueFilterOptions {
|
||||||
type: "active" | "backlog" | null;
|
type: "active" | "backlog" | null;
|
||||||
assignees: string[] | null;
|
assignees: string[] | null;
|
||||||
|
target_date: string[] | null;
|
||||||
state: string[] | null;
|
state: string[] | null;
|
||||||
labels: string[] | null;
|
labels: string[] | null;
|
||||||
issue__assignees__id: string[] | null;
|
issue__assignees__id: string[] | null;
|
||||||
|
4
apps/app/types/projects.d.ts
vendored
4
apps/app/types/projects.d.ts
vendored
@ -80,8 +80,8 @@ type ProjectViewTheme = {
|
|||||||
export interface IProjectMember {
|
export interface IProjectMember {
|
||||||
id: string;
|
id: string;
|
||||||
member: IUserLite;
|
member: IUserLite;
|
||||||
project: IProject;
|
project: IProjectLite;
|
||||||
workspace: IWorkspace;
|
workspace: IWorkspaceLite;
|
||||||
comment: string;
|
comment: string;
|
||||||
role: 5 | 10 | 15 | 20;
|
role: 5 | 10 | 15 | 20;
|
||||||
|
|
||||||
|
@ -1,5 +1,36 @@
|
|||||||
version: "3.8"
|
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:
|
services:
|
||||||
plane-web:
|
plane-web:
|
||||||
container_name: planefrontend
|
container_name: planefrontend
|
||||||
@ -37,35 +68,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
DEBUG: ${DEBUG}
|
<<: *api-and-worker-env
|
||||||
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}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
@ -80,35 +83,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
DEBUG: ${DEBUG}
|
<<: *api-and-worker-env
|
||||||
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}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- plane-api
|
||||||
- plane-db
|
- plane-db
|
||||||
|
Loading…
Reference in New Issue
Block a user