Merge branch 'develop' of github.com:makeplane/plane into feat/notifications

This commit is contained in:
pablohashescobar 2023-07-07 15:17:26 +05:30
commit 9a6026c0c7
83 changed files with 1379 additions and 637 deletions

View File

@ -77,6 +77,13 @@ class ProjectSerializer(BaseSerializer):
raise serializers.ValidationError(detail="Project Identifier is already taken")
class ProjectLiteSerializer(BaseSerializer):
class Meta:
model = Project
fields = ["id", "identifier", "name"]
read_only_fields = fields
class ProjectDetailSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
default_assignee = UserLiteSerializer(read_only=True)
@ -94,7 +101,7 @@ class ProjectDetailSerializer(BaseSerializer):
class ProjectMemberSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
project = ProjectSerializer(read_only=True)
project = ProjectLiteSerializer(read_only=True)
member = UserLiteSerializer(read_only=True)
class Meta:

View File

@ -299,7 +299,6 @@ urlpatterns = [
{
"delete": "destroy",
"get": "retrieve",
"get": "retrieve",
}
),
name="workspace",

View File

@ -263,7 +263,7 @@ class IssueViewSet(BaseViewSet):
return Response(issues, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,

View File

@ -259,7 +259,7 @@ class ProjectViewSet(BaseViewSet):
group="backlog",
description="Default state for managing all Inbox Issues",
project_id=pk,
color="#ff7700"
color="#ff7700",
)
return Response(serializer.data, status=status.HTTP_200_OK)
@ -550,45 +550,47 @@ class AddMemberToProjectEndpoint(BaseAPIView):
def post(self, request, slug, project_id):
try:
member_id = request.data.get("member_id", False)
role = request.data.get("role", False)
members = request.data.get("members", [])
if not member_id or not role:
# get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug)
if not len(members):
return Response(
{"error": "Member ID and role is required"},
{"error": "Atleast one member is required"},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if the user is a member in the workspace
if not WorkspaceMember.objects.filter(
workspace__slug=slug, member_id=member_id
).exists():
# TODO: Update this error message - nk
return Response(
{
"error": "User is not a member of the workspace. Invite the user to the workspace to add him to project"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if the user is already member of project
if ProjectMember.objects.filter(
project=project_id, member_id=member_id
).exists():
return Response(
{"error": "User is already a member of the project"},
status=status.HTTP_400_BAD_REQUEST,
)
# Add the user to project
project_member = ProjectMember.objects.create(
project_id=project_id, member_id=member_id, role=role
project_members = ProjectMember.objects.bulk_create(
[
ProjectMember(
member_id=member.get("member_id"),
role=member.get("role", 10),
project_id=project_id,
workspace_id=project.workspace_id,
)
for member in members
],
batch_size=10,
ignore_conflicts=True,
)
serializer = ProjectMemberSerializer(project_member)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except KeyError:
return Response(
{"error": "Incorrect data sent"}, status=status.HTTP_400_BAD_REQUEST
)
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
return Response(
{"error": "User not member of the workspace"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(

View File

@ -3,6 +3,7 @@ import jwt
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
from uuid import uuid4
# Django imports
from django.db import IntegrityError
from django.db.models import Prefetch
@ -21,6 +22,7 @@ from django.db.models import (
)
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
from django.db.models.fields import DateField
from django.contrib.auth.hashers import make_password
# Third party modules
from rest_framework import status
@ -93,14 +95,33 @@ class WorkSpaceViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
return self.filter_queryset(
super().get_queryset().select_related("owner")
).order_by("name").filter(workspace_member__member=self.request.user).annotate(total_members=member_count).annotate(total_issues=issue_count)
return (
self.filter_queryset(super().get_queryset().select_related("owner"))
.order_by("name")
.filter(workspace_member__member=self.request.user)
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
)
def create(self, request):
try:
serializer = WorkSpaceSerializer(data=request.data)
slug = request.data.get("slug", False)
name = request.data.get("name", False)
if not name or not slug:
return Response(
{"error": "Both name and slug are required"},
status=status.HTTP_400_BAD_REQUEST,
)
if len(name) > 80 or len(slug) > 48:
return Response(
{"error": "The maximum length for name is 80 and for slug is 48"},
status=status.HTTP_400_BAD_REQUEST,
)
if serializer.is_valid():
serializer.save(owner=request.user)
# Create Workspace member
@ -160,14 +181,20 @@ class UserWorkSpacesEndpoint(BaseAPIView):
)
workspace = (
Workspace.objects.prefetch_related(
Prefetch("workspace_member", queryset=WorkspaceMember.objects.all())
(
Workspace.objects.prefetch_related(
Prefetch(
"workspace_member", queryset=WorkspaceMember.objects.all()
)
)
.filter(
workspace_member__member=request.user,
)
.select_related("owner")
)
.filter(
workspace_member__member=request.user,
)
.select_related("owner")
).annotate(total_members=member_count).annotate(total_issues=issue_count)
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
)
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@ -216,9 +243,20 @@ class InviteWorkspaceEndpoint(BaseAPIView):
)
# check for role level
requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user)
if len([email for email in emails if int(email.get("role", 10)) > requesting_user.role]):
return Response({"error": "You cannot invite a user with higher role"}, status=status.HTTP_400_BAD_REQUEST)
requesting_user = WorkspaceMember.objects.get(
workspace__slug=slug, member=request.user
)
if len(
[
email
for email in emails
if int(email.get("role", 10)) > requesting_user.role
]
):
return Response(
{"error": "You cannot invite a user with higher role"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
@ -276,14 +314,18 @@ class InviteWorkspaceEndpoint(BaseAPIView):
# create the user if signup is disabled
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
_ = User.objects.bulk_create([
User(
email=email.get("email"),
password=str(uuid4().hex),
is_password_autoset=True
)
for email in emails
], batch_size=100)
_ = User.objects.bulk_create(
[
User(
username=str(uuid4().hex),
email=invitation.email,
password=make_password(uuid4().hex),
is_password_autoset=True,
)
for invitation in workspace_invitations
],
batch_size=100,
)
for invitation in workspace_invitations:
workspace_invitation.delay(
@ -400,6 +442,30 @@ class WorkspaceInvitationsViewset(BaseViewSet):
.select_related("workspace", "workspace__owner", "created_by")
)
def destroy(self, request, slug, pk):
try:
workspace_member_invite = WorkspaceMemberInvite.objects.get(
pk=pk, workspace__slug=slug
)
# delete the user if signup is disabled
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
user = User.objects.filter(email=workspace_member_invite.email).first()
if user is not None:
user.delete()
workspace_member_invite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceMemberInvite.DoesNotExist:
return Response(
{"error": "Workspace member invite does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UserWorkspaceInvitationsEndpoint(BaseViewSet):
serializer_class = WorkSpaceMemberInviteSerializer
@ -865,7 +931,9 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
)
state_distribution = (
Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user])
Issue.issue_objects.filter(
workspace__slug=slug, assignees__in=[request.user]
)
.annotate(state_group=F("state__group"))
.values("state_group")
.annotate(state_count=Count("state_group"))

View 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",
),
]

View File

@ -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),
),
]

View File

@ -15,15 +15,15 @@ ROLE_CHOICES = (
class Workspace(BaseModel):
name = models.CharField(max_length=255, verbose_name="Workspace Name")
name = models.CharField(max_length=80, verbose_name="Workspace Name")
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="owner_workspace",
)
slug = models.SlugField(max_length=100, db_index=True, unique=True)
company_size = models.PositiveIntegerField(default=10)
slug = models.SlugField(max_length=48, db_index=True, unique=True)
organization_size = models.CharField(max_length=20)
def __str__(self):
"""Return name of the Workspace"""

View File

@ -13,9 +13,7 @@ from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa
# Database
DEBUG = int(os.environ.get(
"DEBUG", 0
)) == 1
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
DATABASES = {
"default": {
@ -72,8 +70,12 @@ CORS_ALLOW_HEADERS = [
]
CORS_ALLOW_CREDENTIALS = True
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
if bool(os.environ.get("SENTRY_DSN", False)):
sentry_sdk.init(
@ -89,7 +91,7 @@ if bool(os.environ.get("SENTRY_DSN", False)):
if DOCKERIZED and USE_MINIO:
INSTALLED_APPS += ("storages",)
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
# The AWS secret access key to use.
@ -97,7 +99,9 @@ if DOCKERIZED and USE_MINIO:
# The name of the bucket to store files in.
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "http://plane-minio:9000")
AWS_S3_ENDPOINT_URL = os.environ.get(
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
)
# Default permissions
AWS_DEFAULT_ACL = "public-read"
AWS_QUERYSTRING_AUTH = False
@ -188,7 +192,10 @@ else:
# extra characters appended.
AWS_S3_FILE_OVERWRITE = False
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# AWS Settings End
# Enable Connection Pooling (if desired)
@ -203,9 +210,6 @@ ALLOWED_HOSTS = [
]
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

View File

@ -48,8 +48,12 @@ ALLOWED_HOSTS = ["*"]
# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD.
CORS_ALLOW_ALL_ORIGINS = True
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Make true if running in a docker environment
DOCKERIZED = int(os.environ.get(
@ -151,7 +155,9 @@ AWS_S3_SIGNATURE_VERSION = None
AWS_S3_FILE_OVERWRITE = False
# AWS Settings End
STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# Enable Connection Pooling (if desired)
# DATABASES['default']['ENGINE'] = 'django_postgrespool'
@ -164,11 +170,6 @@ ALLOWED_HOSTS = [
"*",
]
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

View File

@ -3,11 +3,10 @@
"""
# from django.contrib import admin
from django.urls import path
from django.urls import path, include, re_path
from django.views.generic import TemplateView
from django.conf import settings
from django.conf.urls import include, url, static
# from django.conf.urls.static import static
@ -18,11 +17,10 @@ urlpatterns = [
path("", include("plane.web.urls")),
]
urlpatterns = urlpatterns + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
url(r"^__debug__/", include(debug_toolbar.urls)),
re_path(r"^__debug__/", include(debug_toolbar.urls)),
] + urlpatterns

View File

@ -166,16 +166,16 @@ def filter_target_date(params, filter, method):
for query in target_dates:
target_date_query = query.split(";")
if len(target_date_query) == 2 and "after" in target_date_query:
filter["target_date__gte"] = target_date_query[0]
filter["target_date__gt"] = target_date_query[0]
else:
filter["target_date__lte"] = target_date_query[0]
filter["target_date__lt"] = target_date_query[0]
else:
if params.get("target_date", None) and len(params.get("target_date")):
for query in params.get("target_date"):
if query.get("timeline", "after") == "after":
filter["target_date__gte"] = query.get("datetime")
filter["target_date__gt"] = query.get("datetime")
else:
filter["target_date__lte"] = query.get("datetime")
filter["target_date__lt"] = query.get("datetime")
return filter

View File

@ -1,28 +1,28 @@
# base requirements
Django==3.2.19
Django==4.2.3
django-braces==1.15.0
django-taggit==4.0.0
psycopg2==2.9.6
django-oauth-toolkit==2.3.0
mistune==2.0.4
mistune==3.0.1
djangorestframework==3.14.0
redis==4.6.0
django-nested-admin==4.0.2
django-cors-headers==4.1.0
whitenoise==6.3.0
whitenoise==6.5.0
django-allauth==0.54.0
faker==13.4.0
faker==18.11.2
django-filter==23.2
jsonmodels==2.6.0
djangorestframework-simplejwt==5.2.2
sentry-sdk==1.26.0
sentry-sdk==1.27.0
django-s3-storage==0.14.0
django-crum==0.7.9
django-guardian==2.4.0
dj_rest_auth==2.2.5
google-auth==2.16.0
google-api-python-client==2.75.0
google-auth==2.21.0
google-api-python-client==2.92.0
django-redis==5.3.0
uvicorn==0.22.0
channels==4.0.0

View File

@ -1,3 +1,3 @@
-r base.txt
django-debug-toolbar==3.8.1
django-debug-toolbar==4.1.0

View File

@ -2,11 +2,10 @@
dj-database-url==2.0.0
gunicorn==20.1.0
whitenoise==6.3.0
whitenoise==6.5.0
django-storages==1.13.2
boto3==1.26.163
boto3==1.27.0
django-anymail==10.0
twilio==7.16.2
django-debug-toolbar==3.8.1
django-debug-toolbar==4.1.0
gevent==22.10.2
psycogreen==1.0.2

View File

@ -23,6 +23,7 @@ import {
import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
// helpers
import { renderShortDate } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import {
IAnalyticsParams,
@ -221,7 +222,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<div className="text-sm flex items-center gap-1">
{project.emoji ? (
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">
{String.fromCodePoint(parseInt(project.emoji))}
{renderEmoji(project.emoji)}
</span>
) : project.icon_prop ? (
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
@ -336,7 +337,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<div className="flex items-center gap-1">
{projectDetails?.emoji ? (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">
{String.fromCodePoint(parseInt(projectDetails.emoji))}
{renderEmoji(projectDetails.emoji)}
</div>
) : projectDetails?.icon_prop ? (
<div className="h-6 w-6 grid place-items-center flex-shrink-0">

View File

@ -57,18 +57,6 @@ export const BoardHeader: React.FC<Props> = ({
: null
);
let bgColor = "#000000";
if (selectedGroup === "state") bgColor = currentState?.color ?? "#000000";
if (selectedGroup === "priority")
groupTitle === "high"
? (bgColor = "#dc2626")
: groupTitle === "medium"
? (bgColor = "#f97316")
: groupTitle === "low"
? (bgColor = "#22c55e")
: (bgColor = "#ff0000");
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
@ -96,7 +84,8 @@ export const BoardHeader: React.FC<Props> = ({
switch (selectedGroup) {
case "state":
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
break;
case "priority":
icon = getPriorityIcon(groupTitle, "text-lg");
@ -129,13 +118,13 @@ export const BoardHeader: React.FC<Props> = ({
>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<div
className={`flex cursor-pointer items-center gap-x-3 ${
className={`flex cursor-pointer items-center gap-x-3 max-w-[316px] ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`}
>
<span className="flex items-center">{getGroupIcon()}</span>
<h2
className="text-lg font-semibold capitalize"
className="text-lg font-semibold capitalize truncate"
style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}}

View File

@ -170,7 +170,7 @@ export const CalendarView: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
return calendarIssues ? (
<div className="h-full">
<div className="h-full overflow-y-auto">
<DragDropContext onDragEnd={onDragEnd}>
<div className="h-full rounded-lg p-8 text-brand-secondary">
<CalendarHeader

View File

@ -210,7 +210,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
</a>
</Link>
{displayProperties && (
<div className="relative mt-1.5 flex flex-wrap items-center gap-2 text-xs">
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
@ -225,6 +225,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
className="max-w-full"
isNotAllowed={isNotAllowed}
user={user}
/>

View File

@ -19,7 +19,7 @@ import {
} from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
// helpers
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import 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.old_value;
value = renderShortNumericDateFormat(date as string);
value = renderShortDateWithYearFormat(date as string);
} else if (activity.field === "description") {
value = "description";
} else if (activity.field === "attachment") {

View 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>
);
};

View 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>
);

View File

@ -17,6 +17,7 @@ import stateService from "services/state.service";
// types
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
import { IIssueFilterOptions } from "types";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
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"
>
<span className="capitalize text-brand-secondary">
{replaceUnderscoreIfSnakeCase(key)}:
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
</span>
{filters[key as keyof IIssueFilterOptions] === null ||
(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" />
</button>
</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(", ")
)}
@ -332,6 +378,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
assignees: null,
labels: null,
created_by: null,
target_date: null,
})
}
className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs"

View 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";

View File

@ -2,11 +2,12 @@ import React from "react";
import { useRouter } from "next/router";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useIssuesView from "hooks/use-issues-view";
// headless ui
import { Popover, Transition } from "@headlessui/react";
import useEstimateOption from "hooks/use-estimate-option";
// components
import { SelectFilters } from "components/views";
// ui
@ -17,15 +18,14 @@ import {
ListBulletIcon,
Squares2X2Icon,
CalendarDaysIcon,
ChartBarIcon,
} from "@heroicons/react/24/outline";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { Properties } from "types";
// constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
import useEstimateOption from "hooks/use-estimate-option";
export const IssuesFilterView: React.FC = () => {
const router = useRouter();
@ -109,26 +109,34 @@ export const IssuesFilterView: React.FC = () => {
onSelect={(option) => {
const key = option.key as keyof typeof filters;
const valueExists = filters[key]?.includes(option.value);
if (key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters.target_date ?? [],
option.value
);
if (valueExists) {
setFilters(
{
...(filters ?? {}),
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
},
!Boolean(viewId)
);
setFilters({
target_date: valueExists ? null : option.value,
});
} else {
setFilters(
{
...(filters ?? {}),
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
},
!Boolean(viewId)
);
const valueExists = filters[key]?.includes(option.value);
if (valueExists)
setFilters(
{
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
},
!Boolean(viewId)
);
else
setFilters(
{
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
},
!Boolean(viewId)
);
}
}}
direction="left"
@ -262,9 +270,16 @@ export const IssuesFilterView: React.FC = () => {
if (key === "estimate" && !isEstimateActive) return null;
if (
(issueView === "spreadsheet" && key === "attachment_count") ||
(issueView === "spreadsheet" && key === "link") ||
(issueView === "spreadsheet" && key === "sub_issue_count")
issueView === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
)
return null;
if (
issueView !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;

View File

@ -1,17 +1,12 @@
export * from "./board-view";
export * from "./calendar-view";
export * from "./filters";
export * from "./gantt-chart-view";
export * from "./list-view";
export * from "./modals";
export * from "./spreadsheet-view";
export * from "./sidebar";
export * from "./bulk-delete-issues-modal";
export * from "./existing-issues-list-modal";
export * from "./filters-list";
export * from "./gpt-assistant-modal";
export * from "./image-upload-modal";
export * from "./issues-view-filter";
export * from "./issues-view";
export * from "./link-modal";
export * from "./image-picker-popover";
export * from "./feeds";
export * from "./theme-switch";

View File

@ -38,7 +38,7 @@ export const AllLists: React.FC<Props> = ({
return (
<>
{groupedByIssues && (
<div>
<div className="h-full overflow-y-auto">
{Object.keys(groupedByIssues).map((singleGroup) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;

View File

@ -33,7 +33,6 @@ import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
type?: "issue" | "cycle" | "module";
currentState?: IState | null;
bgColor?: string;
groupTitle: string;
groupedByIssues: {
[key: string]: IIssue[];
@ -53,7 +52,6 @@ type Props = {
export const SingleList: React.FC<Props> = ({
type,
currentState,
bgColor,
groupTitle,
groupedByIssues,
selectedGroup,
@ -113,7 +111,8 @@ export const SingleList: React.FC<Props> = ({
switch (selectedGroup) {
case "state":
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
break;
case "priority":
icon = getPriorityIcon(groupTitle, "text-lg");

View 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";

View File

@ -42,6 +42,7 @@ import {
import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
// helper
import { copyTextToClipboard } from "helpers/string.helper";
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
type Props = {
issue: IIssue;
@ -274,6 +275,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
className="max-w-full"
tooltipPosition={tooltipPosition}
customButton
user={user}
@ -345,6 +347,16 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
/>
</div>
)}
{properties.created_on && (
<div className="flex items-center text-xs cursor-default text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
{renderLongDetailDateFormat(issue.created_at)}
</div>
)}
{properties.updated_on && (
<div className="flex items-center text-xs cursor-default text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
{renderLongDetailDateFormat(issue.updated_at)}
</div>
)}
</div>
);
};

View File

@ -123,7 +123,9 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
<Icon iconName="east" className="text-sm" />
<span>Z</span>
</>
) : col.propertyName === "due_date" ? (
) : col.propertyName === "due_date" ||
col.propertyName === "created_on" ||
col.propertyName === "updated_on" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon

View File

@ -35,7 +35,7 @@ import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helpe
import {
isDateGreaterThanToday,
renderDateFormat,
renderShortDate,
renderShortDateWithYearFormat,
} from "helpers/date-time.helper";
// types
import { ICurrentUserResponse, ICycle } from "types";
@ -315,7 +315,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
>
<CalendarDaysIcon className="h-3 w-3" />
<span>
{renderShortDate(
{renderShortDateWithYearFormat(
new 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" />
<span>
{renderShortDate(
{renderShortDateWithYearFormat(
new Date(
`${watch("end_date") ? watch("end_date") : cycle?.end_date}`
),

View File

@ -10,7 +10,7 @@ import emojis from "./emojis.json";
import icons from "./icons.json";
// helpers
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
import { getRandomEmoji } from "helpers/common.helper";
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
@ -101,7 +101,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange, onIconColorC
setIsOpen(false);
}}
>
{String.fromCodePoint(parseInt(emoji))}
{renderEmoji(emoji)}
</button>
))}
</div>
@ -121,7 +121,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange, onIconColorC
setIsOpen(false);
}}
>
{String.fromCodePoint(parseInt(emoji))}
{renderEmoji(emoji)}
</button>
))}
</div>

View 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>
);

View 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>
);

View File

@ -3,17 +3,17 @@ import React from "react";
import type { Props } from "./types";
export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<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"
fill="#212529"
/>
</svg>
);
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<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"
fill="#858E96"
/>
</svg>
);

View File

@ -4,6 +4,8 @@ export * from "./backlog-state-icon";
export * from "./blocked-icon";
export * from "./blocker-icon";
export * from "./bolt-icon";
export * from "./calendar-before-icon";
export * from "./calendar-after-icon";
export * from "./calendar-month-icon";
export * from "./cancel-icon";
export * from "./cancelled-state-icon";

View File

@ -14,7 +14,7 @@ import {
XCircleIcon,
} from "@heroicons/react/24/outline";
// helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import type { IInboxIssue } from "types";
// constants
@ -72,12 +72,12 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
</div>
</Tooltip>
<Tooltip
tooltipHeading="Created at"
tooltipContent={`${renderShortNumericDateFormat(issue.created_at ?? "")}`}
tooltipHeading="Created on"
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">
<CalendarDaysIcon className="h-3.5 w-3.5" />
<span>{renderShortNumericDateFormat(issue.created_at ?? "")}</span>
<span>{renderShortDateWithYearFormat(issue.created_at ?? "")}</span>
</div>
</Tooltip>
</div>

View File

@ -33,7 +33,7 @@ import {
XCircleIcon,
} from "@heroicons/react/24/outline";
// helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import type { IInboxIssue, IIssue } from "types";
// fetch-keys
@ -252,13 +252,17 @@ export const InboxMainContent: React.FC = () => {
{new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() ? (
<p>
This issue was snoozed till{" "}
{renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")}
{renderShortDateWithYearFormat(
issueDetails.issue_inbox[0].snoozed_till ?? ""
)}
.
</p>
) : (
<p>
This issue has been snoozed till{" "}
{renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")}
{renderShortDateWithYearFormat(
issueDetails.issue_inbox[0].snoozed_till ?? ""
)}
.
</p>
)}

View File

@ -26,7 +26,7 @@ import {
} from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
// helpers
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssueComment, IIssueLabels } from "types";
@ -299,7 +299,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
activityItem.new_value && activityItem.new_value !== ""
? activityItem.new_value
: activityItem.old_value;
value = renderShortNumericDateFormat(date as string);
value = renderShortDateWithYearFormat(date as string);
} else if (activityItem.field === "description") {
value = "description";
} else if (activityItem.field === "attachment") {

View File

@ -5,7 +5,7 @@ import { CalendarDaysIcon, XMarkIcon } from "@heroicons/react/24/outline";
// react-datepicker
import DatePicker from "react-datepicker";
// import "react-datepicker/dist/react-datepicker.css";
import { renderDateFormat } from "helpers/date-time.helper";
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
type Props = {
value: string | null;
@ -20,7 +20,7 @@ export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-brand-secondary">
{value ? (
<>
<span className="text-brand-base">{value}</span>
<span className="text-brand-base">{renderShortDateWithYearFormat(value)}</span>
<button onClick={() => onChange(null)}>
<XMarkIcon className="h-3 w-3" />
</button>

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
// ui
import { CustomDatePicker, Tooltip } from "components/ui";
// helpers
import { findHowManyDaysLeft } from "helpers/date-time.helper";
import { findHowManyDaysLeft, renderShortDateWithYearFormat } from "helpers/date-time.helper";
// services
import trackEventServices from "services/track-event.service";
// types
@ -32,7 +32,9 @@ export const ViewDueDateSelect: React.FC<Props> = ({
return (
<Tooltip
tooltipHeading="Due Date"
tooltipContent={issue.target_date ?? "N/A"}
tooltipContent={
issue.target_date ? renderShortDateWithYearFormat(issue.target_date) ?? "N/A" : "N/A"
}
position={tooltipPosition}
>
<div

View File

@ -22,6 +22,7 @@ type Props = {
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
position?: "left" | "right";
tooltipPosition?: "top" | "bottom";
className?: string;
selfPositioned?: boolean;
customButton?: boolean;
user: ICurrentUserResponse | undefined;
@ -33,6 +34,7 @@ export const ViewStateSelect: React.FC<Props> = ({
partialUpdateIssue,
position = "left",
tooltipPosition = "top",
className = "",
selfPositioned = false,
customButton = false,
user,
@ -68,16 +70,19 @@ export const ViewStateSelect: React.FC<Props> = ({
tooltipContent={addSpaceIfCamelCase(selectedOption?.name ?? "")}
position={tooltipPosition}
>
<div className="flex items-center cursor-pointer gap-2 text-brand-secondary">
{selectedOption &&
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
{selectedOption?.name ?? "State"}
<div className="flex items-center cursor-pointer w-full gap-2 text-brand-secondary">
<span className="h-4 w-4">
{selectedOption &&
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
</span>
<span className="truncate">{selectedOption?.name ?? "State"}</span>
</div>
</Tooltip>
);
return (
<CustomSearchSelect
className={className}
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue(

View File

@ -34,7 +34,7 @@ import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui";
import { ExclamationIcon } from "components/icons";
import { LinkIcon } from "@heroicons/react/20/solid";
// helpers
import { renderDateFormat, renderShortDate } from "helpers/date-time.helper";
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssue, IModule, ModuleLink } from "types";
@ -228,7 +228,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
>
<CalendarDaysIcon className="h-3 w-3" />
<span>
{renderShortDate(new Date(`${module.start_date}`), "Start date")}
{renderShortDateWithYearFormat(
new Date(`${module.start_date}`),
"Start date"
)}
</span>
</Popover.Button>
@ -279,7 +282,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<CalendarDaysIcon className="h-3 w-3 " />
<span>
{renderShortDate(new Date(`${module?.target_date}`), "End date")}
{renderShortDateWithYearFormat(
new Date(`${module?.target_date}`),
"End date"
)}
</span>
</Popover.Button>

View File

@ -21,7 +21,7 @@ import { XMarkIcon } from "@heroicons/react/24/outline";
import { ImagePickerPopover } from "components/core";
import EmojiIconPicker from "components/emoji-icon-picker";
// helpers
import { getRandomEmoji } from "helpers/common.helper";
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
// types
import { ICurrentUserResponse, IProject } from "types";
// fetch-keys
@ -232,7 +232,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
{value.name}
</span>
) : (
String.fromCodePoint(parseInt(value))
renderEmoji(value)
)
) : (
"Icon"

View File

@ -1,14 +1,23 @@
import React from "react";
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { useForm, Controller } from "react-hook-form";
import { useForm, Controller, useFieldArray } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
// ui
import { CustomSelect, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
import {
Avatar,
CustomSearchSelect,
CustomSelect,
PrimaryButton,
SecondaryButton,
} from "components/ui";
//icons
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { ChevronDownIcon } from "@heroicons/react/20/solid";
// services
import projectService from "services/project.service";
import workspaceService from "services/workspace.service";
@ -17,9 +26,9 @@ import { useProjectMyMembership } from "contexts/project-member.context";
// hooks
import useToast from "hooks/use-toast";
// types
import { ICurrentUserResponse, IProjectMemberInvitation } from "types";
import { ICurrentUserResponse } from "types";
// fetch-keys
import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
import { PROJECT_MEMBERS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
@ -30,17 +39,22 @@ type Props = {
user: ICurrentUserResponse | undefined;
};
type ProjectMember = IProjectMemberInvitation & {
type member = {
role: 5 | 10 | 15 | 20;
member_id: string;
user_id: string;
};
const defaultValues: Partial<ProjectMember> = {
email: "",
message: "",
role: 5,
member_id: "",
user_id: "",
type FormValues = {
members: member[];
};
const defaultValues: FormValues = {
members: [
{
role: 5,
member_id: "",
},
],
};
const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, members, user }) => {
@ -56,14 +70,16 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
);
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
setValue,
handleSubmit,
control,
} = useForm<ProjectMember>({
defaultValues,
} = useForm<FormValues>();
const { fields, append, remove } = useFieldArray({
control,
name: "members",
});
const uninvitedPeople = people?.filter((person) => {
@ -71,20 +87,14 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
return !isInvited;
});
const onSubmit = async (formData: ProjectMember) => {
const onSubmit = async (formData: FormValues) => {
if (!workspaceSlug || !projectId || isSubmitting) return;
const payload = { ...formData };
await projectService
.inviteProject(workspaceSlug as string, projectId as string, formData, user)
.then((response) => {
.inviteProject(workspaceSlug as string, projectId as string, payload, user)
.then(() => {
setIsOpen(false);
mutate<any[]>(
PROJECT_INVITATIONS,
(prevData) => {
if (!prevData) return prevData;
return [{ ...formData, ...response }, ...(prevData ?? [])];
},
false
);
mutate(PROJECT_MEMBERS(projectId as string));
setToastAlert({
title: "Success",
type: "success",
@ -93,6 +103,9 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
})
.catch((error) => {
console.log(error);
})
.finally(() => {
reset(defaultValues);
});
};
@ -104,6 +117,35 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
}, 500);
};
const appendField = () => {
append({
role: 5,
member_id: "",
});
};
useEffect(() => {
if (fields.length === 0) {
append([
{
role: 5,
member_id: "",
},
]);
}
}, [fields, append]);
const options = uninvitedPeople?.map((person) => ({
value: person.member.id,
query: person.member.email,
content: (
<div className="flex items-center gap-2">
<Avatar user={person.member} />
{person.member.email}
</div>
),
}));
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@ -116,11 +158,11 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<div className="flex items-center justify-center min-h-full p-4 text-center">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -130,111 +172,138 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
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 transform overflow-hidden rounded-lg bg-brand-surface-2 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-5">
<div className="space-y-5 mb-5">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
Invite Members
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-brand-secondary">
Invite members to work on your project.
</p>
</div>
<div className="text-sm">
<div className="grid grid-cols-12 gap-x-4 mb-3 text-sm">
<h6 className="col-span-7 px-1">Email</h6>
<h6 className="col-span-4 px-1">Role</h6>
</div>
<div className="space-y-3">
<div>
<Controller
control={control}
name="user_id"
rules={{ required: "Please select a member" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
label={
<div
className={`${errors.user_id ? "border-red-500 bg-red-50" : ""}`}
>
{value && value !== ""
? people?.find((p) => p.member.id === value)?.member.email
: "Select email"}
</div>
}
onChange={(val: string) => {
onChange(val);
const person = uninvitedPeople?.find((p) => p.member.id === val);
setValue("member_id", val);
setValue("email", person?.member.email ?? "");
}}
input
width="w-full"
>
{uninvitedPeople && uninvitedPeople.length > 0 ? (
<>
{uninvitedPeople?.map((person) => (
<CustomSelect.Option
key={person.member.id}
value={person.member.id}
>
{person.member.email}
</CustomSelect.Option>
))}
</>
) : (
<div className="text-center text-sm py-5">
Invite members to workspace before adding them to a project.
</div>
<div className="space-y-4 mb-3">
{fields.map((field, index) => (
<div
key={field.id}
className="group grid grid-cols-12 gap-x-4 mb-1 text-sm items-start"
>
<div className="flex flex-col gap-1 col-span-7">
<Controller
control={control}
name={`members.${index}.member_id`}
rules={{ required: "Please select a member" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
customButton={
<button className="flex w-full items-center justify-between gap-1 rounded-md border border-brand-base shadow-sm duration-300 text-brand-secondary hover:text-brand-base hover:bg-brand-surface-2 focus:outline-none px-3 py-2 text-sm text-left">
{value && value !== "" ? (
<div className="flex items-center gap-2">
<Avatar
user={
people?.find((p) => p.member.id === value)?.member
}
/>
{people?.find((p) => p.member.id === value)?.member.email}
</div>
) : (
<div>Select co-worker&rsquo;s email</div>
)}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</button>
}
onChange={(val: string) => {
onChange(val);
}}
options={options}
position="left"
dropdownWidth="w-full min-w-[12rem]"
/>
)}
</CustomSelect>
)}
/>
</div>
<div>
<h6 className="text-brand-secondary">Role</h6>
<Controller
name="role"
control={control}
render={({ field }) => (
<CustomSelect
{...field}
label={
<span className="capitalize">
{field.value ? ROLE[field.value] : "Select role"}
</span>
}
input
width="w-full"
>
{Object.entries(ROLE).map(([key, label]) => {
if (parseInt(key) > (memberDetails?.role ?? 5)) return null;
/>
{errors.members && errors.members[index]?.member_id && (
<span className="text-sm px-1 text-red-500">
{errors.members[index]?.member_id?.message}
</span>
)}
</div>
return (
<CustomSelect.Option key={key} value={key}>
{label}
</CustomSelect.Option>
);
})}
</CustomSelect>
)}
/>
</div>
<div>
<TextArea
id="message"
name="message"
label="Message"
placeholder="Enter message"
error={errors.message}
register={register}
/>
</div>
<div className="flex items-center justify-between gap-2 col-span-5">
<div className="flex flex-col gap-1 w-full">
<Controller
name={`members.${index}.role`}
control={control}
rules={{ required: "Select Role" }}
render={({ field }) => (
<CustomSelect
{...field}
label={
<span className="capitalize">
{field.value ? ROLE[field.value] : "Select role"}
</span>
}
input
width="w-full"
>
{Object.entries(ROLE).map(([key, label]) => {
if (parseInt(key) > (memberDetails?.role ?? 5)) return null;
return (
<CustomSelect.Option key={key} value={key}>
{label}
</CustomSelect.Option>
);
})}
</CustomSelect>
)}
/>
{errors.members && errors.members[index]?.role && (
<span className="text-sm px-1 text-red-500">
{errors.members[index]?.role?.message}
</span>
)}
</div>
<div className="flex flex-item w-6">
{fields.length > 1 && (
<button
type="button"
className="self-center place-items-center rounded"
onClick={() => remove(index)}
>
<XMarkIcon className="h-4 w-4 text-brand-secondary" />
</button>
)}
</div>
</div>
</div>
))}
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Sending Invitation..." : "Send Invitation"}
</PrimaryButton>
<div className="mt-5 flex items-center justify-between gap-2">
<button
type="button"
className="flex items-center gap-2 outline-brand-accent bg-transparent text-brand-accent text-sm font-medium py-2 pr-3"
onClick={appendField}
>
<PlusIcon className="h-4 w-4" />
Add more
</button>
<div className="flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting
? `${
fields && fields.length > 1 ? "Adding Members..." : "Adding Member..."
}`
: `${fields && fields.length > 1 ? "Add Members" : "Add Member"}`}
</PrimaryButton>
</div>
</div>
</form>
</Dialog.Panel>

View File

@ -22,8 +22,9 @@ import {
TrashIcon,
} from "@heroicons/react/24/outline";
// helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import type { IFavoriteProject, IProject } from "types";
// fetch-keys
@ -184,7 +185,7 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
<h3 className="text-1.5xl font-medium text-brand-base">{project.name}</h3>
{project.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{String.fromCodePoint(parseInt(project.emoji))}
{renderEmoji(project.emoji)}
</span>
) : project.icon_prop ? (
<span
@ -202,13 +203,13 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
</Link>
<div className="flex h-full items-end justify-between">
<Tooltip
tooltipContent={`Created at ${renderShortNumericDateFormat(project.created_at)}`}
tooltipContent={`Created at ${renderShortDateWithYearFormat(project.created_at)}`}
position="bottom"
theme="dark"
>
<div className="flex cursor-default items-center gap-1.5 text-xs">
<CalendarDaysIcon className="h-4 w-4" />
{renderShortNumericDateFormat(project.created_at)}
{renderShortDateWithYearFormat(project.created_at)}
</div>
</Tooltip>
{hasJoined ? (

View File

@ -22,6 +22,7 @@ import {
} from "components/icons";
// helpers
import { truncateText } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IProject } from "types";
@ -92,7 +93,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
<div className="flex items-center gap-x-2">
{project.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{String.fromCodePoint(parseInt(project.emoji))}
{renderEmoji(project.emoji)}
</span>
) : project.icon_prop ? (
<div className="h-7 w-7 grid place-items-center">

View File

@ -23,6 +23,7 @@ type CustomSearchSelectProps = {
verticalPosition?: "top" | "bottom";
noChevron?: boolean;
customButton?: JSX.Element;
className?: string;
optionsClassName?: string;
input?: boolean;
disabled?: boolean;
@ -43,6 +44,7 @@ export const CustomSearchSelect = ({
verticalPosition = "bottom",
noChevron = false,
customButton,
className = "",
optionsClassName = "",
input = false,
disabled = false,
@ -70,7 +72,7 @@ export const CustomSearchSelect = ({
return (
<Combobox
as="div"
className={`${!selfPositioned ? "relative" : ""} flex-shrink-0 text-left`}
className={`${!selfPositioned ? "relative" : ""} flex-shrink-0 text-left ${className}`}
{...props}
>
{({ open }: any) => (

View File

@ -5,7 +5,7 @@ import { CalendarDaysIcon, XMarkIcon } from "@heroicons/react/24/outline";
// react-datepicker
import DatePicker from "react-datepicker";
// import "react-datepicker/dist/react-datepicker.css";
import { renderDateFormat } from "helpers/date-time.helper";
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
type Props = {
value: string | null;
@ -21,7 +21,7 @@ export const DateSelect: React.FC<Props> = ({ value, onChange, label }) => (
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-brand-secondary">
{value ? (
<>
<span className="text-brand-base">{value}</span>
<span className="text-brand-base">{renderShortDateWithYearFormat(value)}</span>
<button onClick={() => onChange(null)}>
<XMarkIcon className="h-3 w-3" />
</button>

View File

@ -38,9 +38,9 @@ export const CustomDatePicker: React.FC<Props> = ({
}}
className={`${
renderAs === "input"
? "block px-3 py-2 text-sm focus:outline-none"
? "block px-2 py-2 text-sm focus:outline-none"
: renderAs === "button"
? `px-3 py-1 text-xs shadow-sm ${
? `px-2 py-1 text-xs shadow-sm ${
disabled ? "" : "hover:bg-brand-surface-2"
} duration-300 focus:border-brand-accent focus:outline-none focus:ring-1 focus:ring-brand-accent`
: ""
@ -49,7 +49,7 @@ export const CustomDatePicker: React.FC<Props> = ({
} ${
noBorder ? "" : "border border-brand-base"
} w-full rounded-md bg-transparent caret-transparent ${className}`}
dateFormat="dd-MM-yyyy"
dateFormat="MMM dd, yyyy"
isClearable={isClearable}
disabled={disabled}
/>

View File

@ -18,6 +18,7 @@ type MultiLevelDropdownProps = {
label: string | JSX.Element;
value: any;
selected?: boolean;
element?: JSX.Element;
}[];
}[];
onSelect: (value: any) => void;
@ -35,117 +36,121 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
return (
<Menu as="div" className="relative z-10 inline-block text-left">
{({ open }) => (
<>
<div>
<Menu.Button
onClick={() => setOpenChildFor(null)}
className={`group flex items-center justify-between gap-2 rounded-md border border-brand-base px-3 py-1.5 text-xs shadow-sm duration-300 focus:outline-none ${
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
}`}
<>
<Menu as="div" className="relative z-10 inline-block text-left">
{({ open }) => (
<>
<div>
<Menu.Button
onClick={() => setOpenChildFor(null)}
className={`group flex items-center justify-between gap-2 rounded-md border border-brand-base px-3 py-1.5 text-xs shadow-sm duration-300 focus:outline-none ${
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
}`}
>
{label}
<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}
<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"
>
<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();
<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);
else setOpenChildFor(option.id);
} else {
onSelect(option.value);
}
}}
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"
: ""
}`}
if (openChildFor === option.id) setOpenChildFor(null);
else setOpenChildFor(option.id);
} else {
onSelect(option.value);
}
}}
className="w-full"
>
<div className="space-y-1 p-1">
{option.children.map((child) => (
<button
key={child.id}
type="button"
onClick={() => {
onSelect(child.value);
}}
{({ active }) => (
<>
<div
className={`${
child.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`}
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" : ""
}`}
>
{child.label}
<CheckIcon
className={`h-3.5 w-3.5 opacity-0 ${
child.selected ? "opacity-100" : ""
}`}
/>
</button>
))}
{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-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>
))}
</Menu.Items>
</Transition>
</>
)}
</Menu>
)}
</div>
))}
</Menu.Items>
</Transition>
</>
)}
</Menu>
</>
);
};

View File

@ -1,3 +1,5 @@
import { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
@ -6,18 +8,22 @@ import useSWR from "swr";
import stateService from "services/state.service";
import projectService from "services/project.service";
import issuesService from "services/issues.service";
// components
import { DueDateFilterModal } from "components/core";
// ui
import { Avatar, MultiLevelDropdown } from "components/ui";
// icons
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { IIssueFilterOptions, IQuery } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
// constants
import { PRIORITIES } from "constants/project";
import { DUE_DATES } from "constants/due-dates";
type Props = {
filters: Partial<IIssueFilterOptions> | IQuery;
@ -32,6 +38,8 @@ export const SelectFilters: React.FC<Props> = ({
direction = "right",
height = "md",
}) => {
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -58,125 +66,163 @@ export const SelectFilters: React.FC<Props> = ({
);
return (
<MultiLevelDropdown
label="Filters"
onSelect={onSelect}
direction={direction}
height={height}
options={[
{
id: "priority",
label: "Priority",
value: PRIORITIES,
children: [
...PRIORITIES.map((priority) => ({
id: priority === null ? "null" : priority,
label: (
<div className="flex items-center gap-2">
{getPriorityIcon(priority)} {priority ?? "None"}
</div>
),
value: {
key: "priority",
value: priority === null ? "null" : priority,
<>
{isDueDateFilterModalOpen && (
<DueDateFilterModal
isOpen={isDueDateFilterModalOpen}
handleClose={() => setIsDueDateFilterModalOpen(false)}
/>
)}
<MultiLevelDropdown
label="Filters"
onSelect={onSelect}
direction={direction}
height={height}
options={[
{
id: "priority",
label: "Priority",
value: PRIORITIES,
children: [
...PRIORITIES.map((priority) => ({
id: 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),
})) ?? []),
],
},
]}
/>
],
},
]}
/>
</>
);
};

View File

@ -18,7 +18,7 @@ import { VIEWS_LIST } from "constants/fetch-keys";
import useToast from "hooks/use-toast";
// helpers
import { truncateText } from "helpers/string.helper";
import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
import { renderShortDateWithYearFormat, renderShortTime } from "helpers/date-time.helper";
type Props = {
view: IView;
@ -107,7 +107,7 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
<Tooltip
tooltipContent={`Last updated at ${renderShortTime(
view.updated_at
)} ${renderShortDate(view.updated_at)}`}
)} ${renderShortDateWithYearFormat(view.updated_at)}`}
>
<p className="text-sm text-brand-secondary">
{renderShortTime(view.updated_at)}

View File

@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
// ui
import { Tooltip } from "components/ui";
// helpers
import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-time.helper";
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IUserActivity } from "types";
// constants
@ -109,7 +109,7 @@ export const ActivityGraph: React.FC<Props> = ({ activities }) => {
key={`${date}-${index}`}
tooltipContent={`${
isActive ? isActive.activity_count : 0
} activities on ${renderShortNumericDateFormat(date)}`}
} activities on ${renderShortDateWithYearFormat(date)}`}
theme="dark"
>
<div

View 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`,
],
},
];

View File

@ -1,3 +1,4 @@
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
export const GROUP_CHOICES = {

View File

@ -59,4 +59,20 @@ export const SPREADSHEET_COLUMN = [
ascendingOrder: "estimate_point",
descendingOrder: "-estimate_point",
},
{
propertyName: "created_on",
colName: "Created On",
colSize: "144px",
icon: CalendarDaysIcon,
ascendingOrder: "-created_at",
descendingOrder: "created_at",
},
{
propertyName: "updated_on",
colName: "Updated On",
colSize: "144px",
icon: CalendarDaysIcon,
ascendingOrder: "-updated_at",
descendingOrder: "updated_at",
},
];

View File

@ -89,6 +89,7 @@ export const initialState: StateType = {
issue__assignees__id: null,
issue__labels__id: null,
created_by: null,
target_date: null,
},
};

View File

@ -42,3 +42,11 @@ export const findStringWithMostCharacters = (strings: string[]) =>
strings.reduce((longestString, currentString) =>
currentString.length > longestString.length ? currentString : longestString
);
export const checkIfArraysHaveSameElements = (arr1: any[] | null, arr2: any[] | null): boolean => {
if (!arr1 || !arr2) return false;
if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false;
if (arr1.length === 0 && arr2.length === 0) return true;
return arr1.length === arr2.length && arr1.every((e) => arr2.includes(e));
};

View File

@ -16,23 +16,3 @@ export const debounce = (func: any, wait: number, immediate: boolean = false) =>
if (callNow) func(...args);
};
};
export const getRandomEmoji = () => {
const emojis = [
"8986",
"9200",
"128204",
"127773",
"127891",
"127947",
"128076",
"128077",
"128187",
"128188",
"128512",
"128522",
"128578",
];
return emojis[Math.floor(Math.random() * emojis.length)];
};

View File

@ -18,6 +18,13 @@ export const renderShortNumericDateFormat = (date: string | Date) =>
month: "short",
});
export const renderLongDetailDateFormat = (date: string | Date) =>
new Date(date).toLocaleDateString("en-UK", {
day: "numeric",
month: "long",
year: "numeric",
});
export const findHowManyDaysLeft = (date: string | Date) => {
const today = new Date();
const eventDate = new Date(date);
@ -114,7 +121,7 @@ export const getDateRangeStatus = (
}
};
export const renderShortDateWithYearFormat = (date: string | Date) => {
export const renderShortDateWithYearFormat = (date: string | Date, placeholder?: string) => {
if (!date || date === "") return null;
date = new Date(date);
@ -136,7 +143,8 @@ export const renderShortDateWithYearFormat = (date: string | Date) => {
const day = date.getDate();
const month = months[date.getMonth()];
const year = date.getFullYear();
return isNaN(date.getTime()) ? "N/A" : ` ${month} ${day}, ${year}`;
return isNaN(date.getTime()) ? placeholder ?? "N/A" : ` ${month} ${day}, ${year}`;
};
export const renderShortDate = (date: string | Date, placeholder?: string) => {

View File

@ -0,0 +1,25 @@
export const getRandomEmoji = () => {
const emojis = [
"8986",
"9200",
"128204",
"127773",
"127891",
"127947",
"128076",
"128077",
"128187",
"128188",
"128512",
"128522",
"128578",
];
return emojis[Math.floor(Math.random() * emojis.length)];
};
export const renderEmoji = (emoji: string) => {
if (!emoji) return;
return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
};

View File

@ -18,6 +18,8 @@ const initialValues: Properties = {
attachment_count: false,
link: false,
estimate: false,
created_on: false,
updated_on: false,
};
const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
@ -96,6 +98,8 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
attachment_count: properties.attachment_count,
link: properties.link,
estimate: properties.estimate,
created_on: properties.created_on,
updated_on: properties.updated_on,
};
return [newProperties, updateIssueProperties] as const;

View File

@ -60,6 +60,7 @@ const useIssuesView = () => {
? filters?.issue__labels__id.join(",")
: undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
};
const { data: projectIssues } = useSWR(

View File

@ -19,6 +19,8 @@ const initialValues: Properties = {
attachment_count: false,
link: false,
estimate: false,
created_on: false,
updated_on: false,
};
const useMyIssuesProperties = (workspaceSlug?: string) => {
@ -92,6 +94,8 @@ const useMyIssuesProperties = (workspaceSlug?: string) => {
attachment_count: properties.attachment_count,
link: properties.link,
estimate: properties.estimate,
created_on: properties.created_on,
updated_on: properties.updated_on,
};
return [newProperties, updateIssueProperties] as const;

View File

@ -159,7 +159,7 @@ const SingleCycle: React.FC = () => {
>
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
<div
className={`h-full ${cycleSidebar ? "mr-[24rem]" : ""} ${
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
analyticsModal ? "mr-[50%]" : ""
} duration-300`}
>

View File

@ -99,7 +99,9 @@ const ProjectIssues: NextPage = () => {
}
>
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
<IssuesView />
<div className="h-full w-full flex flex-col">
<IssuesView />
</div>
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
);

View File

@ -164,7 +164,7 @@ const SingleModule: React.FC = () => {
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
<div
className={`h-full ${moduleSidebar ? "mr-[24rem]" : ""} ${
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${
analyticsModal ? "mr-[50%]" : ""
} duration-300`}
>

View File

@ -124,7 +124,29 @@ const ProjectPages: NextPage = () => {
}
>
<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
as={Fragment}
defaultIndex={currentTabValue(pageTab)}
@ -147,7 +169,7 @@ const ProjectPages: NextPage = () => {
}}
>
<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) => (
<Tab
key={`${tab}-${index}`}
@ -163,26 +185,6 @@ const ProjectPages: NextPage = () => {
</Tab>
))}
</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.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full overflow-y-auto space-y-5">

View File

@ -27,6 +27,8 @@ import {
DangerButton,
} from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IProject, IWorkspace } from "types";
import type { NextPage } from "next";
@ -186,7 +188,7 @@ const GeneralSettings: NextPage = () => {
{value.name}
</span>
) : (
String.fromCodePoint(parseInt(value))
renderEmoji(value)
)
) : (
"Icon"

View File

@ -101,7 +101,9 @@ const SingleView: React.FC = () => {
</div>
}
>
<IssuesView />
<div className="h-full w-full flex flex-col">
<IssuesView />
</div>
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
);

View File

@ -8,6 +8,7 @@ import type {
ICurrentUserResponse,
IFavoriteProject,
IProject,
IProjectBulkInviteFormData,
IProjectMember,
IProjectMemberInvitation,
ISearchIssueResponse,
@ -102,7 +103,7 @@ class ProjectServices extends APIService {
async inviteProject(
workspaceSlug: string,
projectId: string,
data: any,
data: IProjectBulkInviteFormData,
user: ICurrentUserResponse | undefined
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/add/`, data)

View File

@ -179,25 +179,6 @@ body {
outline: none;
}
/* react datepicker styling */
.react-datepicker-wrapper input::placeholder {
color: rgba(var(--color-text-secondary));
opacity: 1;
}
.react-datepicker-wrapper input:-ms-input-placeholder {
color: rgba(var(--color-text-secondary));
}
.react-datepicker-wrapper .react-datepicker__close-icon::after {
background: transparent;
color: rgba(var(--color-text-secondary));
}
.react-datepicker-popper {
z-index: 30 !important;
}
.conical-gradient {
background: conic-gradient(
from 180deg at 50% 50%,

View File

@ -81,7 +81,7 @@
}
.react-datepicker__day-name {
color: rgba(var(--color-text-base)) !important;
color: rgba(var(--color-text-secondary)) !important;
}
.react-datepicker__week {

View File

@ -188,6 +188,8 @@ export type Properties = {
link: boolean;
attachment_count: boolean;
estimate: boolean;
created_on: boolean;
updated_on: boolean;
};
export interface IIssueLabels {
@ -239,6 +241,7 @@ export interface IIssueLite {
export interface IIssueFilterOptions {
type: "active" | "backlog" | null;
assignees: string[] | null;
target_date: string[] | null;
state: string[] | null;
labels: string[] | null;
issue__assignees__id: string[] | null;

View File

@ -80,8 +80,8 @@ type ProjectViewTheme = {
export interface IProjectMember {
id: string;
member: IUserLite;
project: IProject;
workspace: IWorkspace;
project: IProjectLite;
workspace: IWorkspaceLite;
comment: string;
role: 5 | 10 | 15 | 20;
@ -113,6 +113,10 @@ export interface IProjectMemberInvitation {
updated_by: string;
}
export interface IProjectBulkInviteFormData {
members: { role: 5 | 10 | 15 | 20; member_id: string }[];
}
export interface IGithubRepository {
id: string;
full_name: string;

View File

@ -45,6 +45,8 @@ export type Properties = {
link: boolean;
attachment_count: boolean;
estimate: boolean;
created_on: boolean;
updated_on: boolean;
};
export interface IWorkspaceMember {