forked from github/plane
Merge pull request #1600 from makeplane/stage-release
promote: stage-release to master
This commit is contained in:
commit
11faf3f810
@ -59,8 +59,9 @@ AWS_S3_BUCKET_NAME="uploads"
|
|||||||
FILE_SIZE_LIMIT=5242880
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
||||||
# GPT settings
|
# GPT settings
|
||||||
OPENAI_API_KEY=""
|
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
||||||
GPT_ENGINE=""
|
OPENAI_API_KEY="sk-" # add your openai key here
|
||||||
|
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
||||||
|
|
||||||
# Github
|
# Github
|
||||||
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||||
|
@ -23,7 +23,6 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
|
|||||||
- Python version 3.8+
|
- Python version 3.8+
|
||||||
- Postgres version v14
|
- Postgres version v14
|
||||||
- Redis version v6.2.7
|
- Redis version v6.2.7
|
||||||
- pnpm version 7.22.0
|
|
||||||
|
|
||||||
### Setup the project
|
### Setup the project
|
||||||
|
|
||||||
|
24
README.md
24
README.md
@ -19,14 +19,14 @@
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
|
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
|
||||||
<img
|
<img
|
||||||
src="https://ik.imagekit.io/killbluedog/Plane_Screen.png?updatedAt=1684942001069"
|
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Screen.png"
|
||||||
alt="Plane Screens"
|
alt="Plane Screens"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://app.plane.so/#gh-dark-mode-only" target="_blank">
|
<a href="https://app.plane.so/#gh-dark-mode-only" target="_blank">
|
||||||
<img
|
<img
|
||||||
src="https://ik.imagekit.io/killbluedog/Plane_Screens_Dark_Mode.png?updatedAt=1684942388044"
|
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Screens+Dark+Mode.png"
|
||||||
alt="Plane Screens"
|
alt="Plane Screens"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
@ -61,14 +61,6 @@ chmod +x setup.sh
|
|||||||
|
|
||||||
> If running in a cloud env replace localhost with public facing IP address of the VM
|
> If running in a cloud env replace localhost with public facing IP address of the VM
|
||||||
|
|
||||||
- Export Environment Variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
set -a
|
|
||||||
source .env
|
|
||||||
set +a
|
|
||||||
```
|
|
||||||
|
|
||||||
- Run Docker compose up
|
- Run Docker compose up
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -94,7 +86,7 @@ docker compose up -d
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://plane.so" target="_blank">
|
<a href="https://plane.so" target="_blank">
|
||||||
<img
|
<img
|
||||||
src="https://ik.imagekit.io/killbluedog/Plane_Views_Dark_Mode.png?updatedAt=1684943050275"
|
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Views+Dark+Mode.png"
|
||||||
alt="Plane Views"
|
alt="Plane Views"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
@ -103,7 +95,7 @@ docker compose up -d
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://plane.so" target="_blank">
|
<a href="https://plane.so" target="_blank">
|
||||||
<img
|
<img
|
||||||
src="https://ik.imagekit.io/killbluedog/Plane_Issue_Detail_Dark_Mode.png?updatedAt=1684943050202"
|
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Issue+Detail+Dark+Mode.png"
|
||||||
alt="Plane Issue Details"
|
alt="Plane Issue Details"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
@ -112,7 +104,7 @@ docker compose up -d
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://plane.so" target="_blank">
|
<a href="https://plane.so" target="_blank">
|
||||||
<img
|
<img
|
||||||
src="https://ik.imagekit.io/killbluedog/Plane_Cycles___Modules_Dark_Mode.png?updatedAt=1684943050281"
|
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Cycles+%26+Modules+Dark+Mode.png"
|
||||||
alt="Plane Cycles and Modules"
|
alt="Plane Cycles and Modules"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
@ -121,7 +113,7 @@ docker compose up -d
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://plane.so" target="_blank">
|
<a href="https://plane.so" target="_blank">
|
||||||
<img
|
<img
|
||||||
src="https://ik.imagekit.io/killbluedog/Plane_Analytics_Dark_Mode.png?updatedAt=1684944596824"
|
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Analytics+Dark+Mode.png"
|
||||||
alt="Plane Analytics"
|
alt="Plane Analytics"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
@ -130,7 +122,7 @@ docker compose up -d
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://plane.so" target="_blank">
|
<a href="https://plane.so" target="_blank">
|
||||||
<img
|
<img
|
||||||
src="https://ik.imagekit.io/killbluedog/Plane_Pages_Dark_Mode.png?updatedAt=1684943050202"
|
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Pages+Dark+Mode.png"
|
||||||
alt="Plane Pages"
|
alt="Plane Pages"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
@ -140,7 +132,7 @@ docker compose up -d
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://plane.so" target="_blank">
|
<a href="https://plane.so" target="_blank">
|
||||||
<img
|
<img
|
||||||
src="https://ik.imagekit.io/killbluedog/Plane_Commad_K_Dark_Mode.png?updatedAt=1684943050312"
|
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Commad+K+Dark+Mode.png"
|
||||||
alt="Plane Command Menu"
|
alt="Plane Command Menu"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
|
@ -49,7 +49,7 @@ USER root
|
|||||||
RUN apk --no-cache add "bash~=5.2"
|
RUN apk --no-cache add "bash~=5.2"
|
||||||
COPY ./bin ./bin/
|
COPY ./bin ./bin/
|
||||||
|
|
||||||
RUN chmod +x ./bin/takeoff ./bin/worker
|
RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
|
||||||
RUN chmod -R 777 /code
|
RUN chmod -R 777 /code
|
||||||
|
|
||||||
USER captain
|
USER captain
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
|
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
|
||||||
worker: celery -A plane worker -l info
|
worker: celery -A plane worker -l info
|
||||||
|
beat: celery -A plane beat -l INFO
|
5
apiserver/bin/beat
Normal file
5
apiserver/bin/beat
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
python manage.py wait_for_db
|
||||||
|
celery -A plane beat -l info
|
@ -6,4 +6,4 @@ python manage.py migrate
|
|||||||
# Create a Default User
|
# Create a Default User
|
||||||
python bin/user_script.py
|
python bin/user_script.py
|
||||||
|
|
||||||
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --config gunicorn.config.py --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
||||||
|
@ -21,6 +21,7 @@ from .project import (
|
|||||||
ProjectIdentifierSerializer,
|
ProjectIdentifierSerializer,
|
||||||
ProjectFavoriteSerializer,
|
ProjectFavoriteSerializer,
|
||||||
ProjectLiteSerializer,
|
ProjectLiteSerializer,
|
||||||
|
ProjectMemberLiteSerializer,
|
||||||
)
|
)
|
||||||
from .state import StateSerializer, StateLiteSerializer
|
from .state import StateSerializer, StateLiteSerializer
|
||||||
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
||||||
@ -41,6 +42,7 @@ from .issue import (
|
|||||||
IssueLinkSerializer,
|
IssueLinkSerializer,
|
||||||
IssueLiteSerializer,
|
IssueLiteSerializer,
|
||||||
IssueAttachmentSerializer,
|
IssueAttachmentSerializer,
|
||||||
|
IssueSubscriberSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .module import (
|
from .module import (
|
||||||
@ -74,4 +76,7 @@ from .estimate import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
|
from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
|
||||||
|
|
||||||
from .analytic import AnalyticViewSerializer
|
from .analytic import AnalyticViewSerializer
|
||||||
|
|
||||||
|
from .notification import NotificationSerializer
|
||||||
|
@ -19,6 +19,7 @@ from plane.db.models import (
|
|||||||
IssueProperty,
|
IssueProperty,
|
||||||
IssueBlocker,
|
IssueBlocker,
|
||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
|
IssueSubscriber,
|
||||||
IssueLabel,
|
IssueLabel,
|
||||||
Label,
|
Label,
|
||||||
IssueBlocker,
|
IssueBlocker,
|
||||||
@ -461,9 +462,9 @@ class IssueAttachmentSerializer(BaseSerializer):
|
|||||||
|
|
||||||
# Issue Serializer with state details
|
# Issue Serializer with state details
|
||||||
class IssueStateSerializer(BaseSerializer):
|
class IssueStateSerializer(BaseSerializer):
|
||||||
state_detail = StateSerializer(read_only=True, source="state")
|
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
bridge_id = serializers.UUIDField(read_only=True)
|
bridge_id = serializers.UUIDField(read_only=True)
|
||||||
@ -476,7 +477,7 @@ class IssueStateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueSerializer(BaseSerializer):
|
class IssueSerializer(BaseSerializer):
|
||||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
state_detail = StateSerializer(read_only=True, source="state")
|
state_detail = StateSerializer(read_only=True, source="state")
|
||||||
parent_detail = IssueFlatSerializer(read_only=True, source="parent")
|
parent_detail = IssueFlatSerializer(read_only=True, source="parent")
|
||||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||||
@ -530,3 +531,14 @@ class IssueLiteSerializer(BaseSerializer):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class IssueSubscriberSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = IssueSubscriber
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"issue",
|
||||||
|
]
|
||||||
|
@ -106,7 +106,7 @@ class ModuleFlatSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class ModuleIssueSerializer(BaseSerializer):
|
class ModuleIssueSerializer(BaseSerializer):
|
||||||
module_detail = ModuleFlatSerializer(read_only=True, source="module")
|
module_detail = ModuleFlatSerializer(read_only=True, source="module")
|
||||||
issue_detail = IssueStateSerializer(read_only=True, source="issue")
|
issue_detail = ProjectLiteSerializer(read_only=True, source="issue")
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -151,7 +151,7 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleSerializer(BaseSerializer):
|
class ModuleSerializer(BaseSerializer):
|
||||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
||||||
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
|
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
|
||||||
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
||||||
|
12
apiserver/plane/api/serializers/notification.py
Normal file
12
apiserver/plane/api/serializers/notification.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Module imports
|
||||||
|
from .base import BaseSerializer
|
||||||
|
from .user import UserLiteSerializer
|
||||||
|
from plane.db.models import Notification
|
||||||
|
|
||||||
|
class NotificationSerializer(BaseSerializer):
|
||||||
|
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Notification
|
||||||
|
fields = "__all__"
|
||||||
|
|
@ -77,6 +77,13 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
raise serializers.ValidationError(detail="Project Identifier is already taken")
|
raise serializers.ValidationError(detail="Project Identifier is already taken")
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectLiteSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Project
|
||||||
|
fields = ["id", "identifier", "name"]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetailSerializer(BaseSerializer):
|
class ProjectDetailSerializer(BaseSerializer):
|
||||||
workspace = WorkSpaceSerializer(read_only=True)
|
workspace = WorkSpaceSerializer(read_only=True)
|
||||||
default_assignee = UserLiteSerializer(read_only=True)
|
default_assignee = UserLiteSerializer(read_only=True)
|
||||||
@ -94,7 +101,7 @@ class ProjectDetailSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class ProjectMemberSerializer(BaseSerializer):
|
class ProjectMemberSerializer(BaseSerializer):
|
||||||
workspace = WorkSpaceSerializer(read_only=True)
|
workspace = WorkSpaceSerializer(read_only=True)
|
||||||
project = ProjectSerializer(read_only=True)
|
project = ProjectLiteSerializer(read_only=True)
|
||||||
member = UserLiteSerializer(read_only=True)
|
member = UserLiteSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -103,8 +110,8 @@ class ProjectMemberSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||||
project = ProjectSerializer(read_only=True)
|
project = ProjectLiteSerializer(read_only=True)
|
||||||
workspace = WorkSpaceSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectMemberInvite
|
model = ProjectMemberInvite
|
||||||
@ -118,7 +125,7 @@ class ProjectIdentifierSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectFavoriteSerializer(BaseSerializer):
|
class ProjectFavoriteSerializer(BaseSerializer):
|
||||||
project_detail = ProjectSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectFavorite
|
model = ProjectFavorite
|
||||||
@ -129,8 +136,13 @@ class ProjectFavoriteSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProjectLiteSerializer(BaseSerializer):
|
|
||||||
|
|
||||||
|
class ProjectMemberLiteSerializer(BaseSerializer):
|
||||||
|
member = UserLiteSerializer(read_only=True)
|
||||||
|
is_subscribed = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = ProjectMember
|
||||||
fields = ["id", "identifier", "name"]
|
fields = ["member", "id", "is_subscribed"]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
@ -22,6 +22,7 @@ from plane.api.views import (
|
|||||||
# User
|
# User
|
||||||
UserEndpoint,
|
UserEndpoint,
|
||||||
UpdateUserOnBoardedEndpoint,
|
UpdateUserOnBoardedEndpoint,
|
||||||
|
UpdateUserTourCompletedEndpoint,
|
||||||
UserActivityEndpoint,
|
UserActivityEndpoint,
|
||||||
## End User
|
## End User
|
||||||
# Workspaces
|
# Workspaces
|
||||||
@ -76,6 +77,8 @@ from plane.api.views import (
|
|||||||
IssueLinkViewSet,
|
IssueLinkViewSet,
|
||||||
BulkCreateIssueLabelsEndpoint,
|
BulkCreateIssueLabelsEndpoint,
|
||||||
IssueAttachmentEndpoint,
|
IssueAttachmentEndpoint,
|
||||||
|
IssueArchiveViewSet,
|
||||||
|
IssueSubscriberViewSet,
|
||||||
## End Issues
|
## End Issues
|
||||||
# States
|
# States
|
||||||
StateViewSet,
|
StateViewSet,
|
||||||
@ -148,6 +151,10 @@ from plane.api.views import (
|
|||||||
ExportAnalyticsEndpoint,
|
ExportAnalyticsEndpoint,
|
||||||
DefaultAnalyticsEndpoint,
|
DefaultAnalyticsEndpoint,
|
||||||
## End Analytics
|
## End Analytics
|
||||||
|
# Notification
|
||||||
|
NotificationViewSet,
|
||||||
|
UnreadNotificationEndpoint,
|
||||||
|
## End Notification
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -197,7 +204,12 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"users/me/onboard/",
|
"users/me/onboard/",
|
||||||
UpdateUserOnBoardedEndpoint.as_view(),
|
UpdateUserOnBoardedEndpoint.as_view(),
|
||||||
name="change-password",
|
name="user-onboard",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/me/tour-completed/",
|
||||||
|
UpdateUserTourCompletedEndpoint.as_view(),
|
||||||
|
name="user-tour",
|
||||||
),
|
),
|
||||||
path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
|
path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
|
||||||
# user workspaces
|
# user workspaces
|
||||||
@ -467,7 +479,6 @@ urlpatterns = [
|
|||||||
"workspaces/<str:slug>/user-favorite-projects/",
|
"workspaces/<str:slug>/user-favorite-projects/",
|
||||||
ProjectFavoritesViewSet.as_view(
|
ProjectFavoritesViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "list",
|
|
||||||
"post": "create",
|
"post": "create",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -797,6 +808,34 @@ urlpatterns = [
|
|||||||
name="project-issue-comment",
|
name="project-issue-comment",
|
||||||
),
|
),
|
||||||
## End IssueComments
|
## End IssueComments
|
||||||
|
# Issue Subscribers
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
|
||||||
|
IssueSubscriberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-subscribers",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
|
||||||
|
IssueSubscriberViewSet.as_view({"delete": "destroy"}),
|
||||||
|
name="project-issue-subscribers",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
|
||||||
|
IssueSubscriberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "subscription_status",
|
||||||
|
"post": "subscribe",
|
||||||
|
"delete": "unsubscribe",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-subscribers",
|
||||||
|
),
|
||||||
|
## End Issue Subscribers
|
||||||
## IssueProperty
|
## IssueProperty
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
|
||||||
@ -821,6 +860,36 @@ urlpatterns = [
|
|||||||
name="project-issue-roadmap",
|
name="project-issue-roadmap",
|
||||||
),
|
),
|
||||||
## IssueProperty Ebd
|
## IssueProperty Ebd
|
||||||
|
## Issue Archives
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
|
||||||
|
IssueArchiveViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-archive",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/",
|
||||||
|
IssueArchiveViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-archive",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/unarchive/<uuid:pk>/",
|
||||||
|
IssueArchiveViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "unarchive",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-archive",
|
||||||
|
),
|
||||||
|
## End Issue Archives
|
||||||
## File Assets
|
## File Assets
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/file-assets/",
|
"workspaces/<str:slug>/file-assets/",
|
||||||
@ -1273,4 +1342,51 @@ urlpatterns = [
|
|||||||
name="default-analytics",
|
name="default-analytics",
|
||||||
),
|
),
|
||||||
## End Analytics
|
## End Analytics
|
||||||
|
# Notification
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/",
|
||||||
|
NotificationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="notifications",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
|
||||||
|
NotificationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="notifications",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
|
||||||
|
NotificationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "mark_read",
|
||||||
|
"delete": "mark_unread",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="notifications",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
|
||||||
|
NotificationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "archive",
|
||||||
|
"delete": "unarchive",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="notifications",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/unread/",
|
||||||
|
UnreadNotificationEndpoint.as_view(),
|
||||||
|
name="unread-notifications",
|
||||||
|
),
|
||||||
|
## End Notification
|
||||||
]
|
]
|
||||||
|
@ -16,6 +16,7 @@ from .project import (
|
|||||||
from .people import (
|
from .people import (
|
||||||
UserEndpoint,
|
UserEndpoint,
|
||||||
UpdateUserOnBoardedEndpoint,
|
UpdateUserOnBoardedEndpoint,
|
||||||
|
UpdateUserTourCompletedEndpoint,
|
||||||
UserActivityEndpoint,
|
UserActivityEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -65,6 +66,8 @@ from .issue import (
|
|||||||
IssueLinkViewSet,
|
IssueLinkViewSet,
|
||||||
BulkCreateIssueLabelsEndpoint,
|
BulkCreateIssueLabelsEndpoint,
|
||||||
IssueAttachmentEndpoint,
|
IssueAttachmentEndpoint,
|
||||||
|
IssueArchiveViewSet,
|
||||||
|
IssueSubscriberViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .auth_extended import (
|
from .auth_extended import (
|
||||||
@ -133,6 +136,7 @@ from .estimate import (
|
|||||||
from .release import ReleaseNotesEndpoint
|
from .release import ReleaseNotesEndpoint
|
||||||
|
|
||||||
from .inbox import InboxViewSet, InboxIssueViewSet
|
from .inbox import InboxViewSet, InboxIssueViewSet
|
||||||
|
|
||||||
from .analytic import (
|
from .analytic import (
|
||||||
AnalyticsEndpoint,
|
AnalyticsEndpoint,
|
||||||
AnalyticViewViewset,
|
AnalyticViewViewset,
|
||||||
@ -140,3 +144,5 @@ from .analytic import (
|
|||||||
ExportAnalyticsEndpoint,
|
ExportAnalyticsEndpoint,
|
||||||
DefaultAnalyticsEndpoint,
|
DefaultAnalyticsEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .notification import NotificationViewSet, UnreadNotificationEndpoint
|
@ -345,7 +345,7 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
user_token = request.data.get("token", "").strip().lower()
|
user_token = request.data.get("token", "").strip()
|
||||||
key = request.data.get("key", False)
|
key = request.data.get("key", False)
|
||||||
|
|
||||||
if not key or user_token == "":
|
if not key or user_token == "":
|
||||||
|
@ -706,9 +706,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class CycleFavoriteViewSet(BaseViewSet):
|
class CycleFavoriteViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
serializer_class = CycleFavoriteSerializer
|
serializer_class = CycleFavoriteSerializer
|
||||||
model = CycleFavorite
|
model = CycleFavorite
|
||||||
|
@ -30,31 +30,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
# If logger is enabled check for request limit
|
|
||||||
if settings.LOGGER_BASE_URL:
|
|
||||||
try:
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
settings.LOGGER_BASE_URL,
|
|
||||||
json={"user_id": str(request.user.id)},
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
count = response.json().get("count", 0)
|
|
||||||
if not response.json().get("success", False):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "You have surpassed the monthly limit for AI assistance"
|
|
||||||
},
|
|
||||||
status=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
|
|
||||||
prompt = request.data.get("prompt", False)
|
prompt = request.data.get("prompt", False)
|
||||||
task = request.data.get("task", False)
|
task = request.data.get("task", False)
|
||||||
|
|
||||||
@ -67,7 +42,7 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
openai.api_key = settings.OPENAI_API_KEY
|
openai.api_key = settings.OPENAI_API_KEY
|
||||||
response = openai.Completion.create(
|
response = openai.Completion.create(
|
||||||
engine=settings.GPT_ENGINE,
|
model=settings.GPT_ENGINE,
|
||||||
prompt=final_text,
|
prompt=final_text,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
max_tokens=1024,
|
max_tokens=1024,
|
||||||
@ -82,7 +57,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
|||||||
{
|
{
|
||||||
"response": text,
|
"response": text,
|
||||||
"response_html": text_html,
|
"response_html": text_html,
|
||||||
"count": count,
|
|
||||||
"project_detail": ProjectLiteSerializer(project).data,
|
"project_detail": ProjectLiteSerializer(project).data,
|
||||||
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
|
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
|
||||||
},
|
},
|
||||||
|
@ -15,6 +15,7 @@ from django.db.models import (
|
|||||||
Value,
|
Value,
|
||||||
CharField,
|
CharField,
|
||||||
When,
|
When,
|
||||||
|
Exists,
|
||||||
Max,
|
Max,
|
||||||
)
|
)
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
@ -43,11 +44,14 @@ from plane.api.serializers import (
|
|||||||
IssueLinkSerializer,
|
IssueLinkSerializer,
|
||||||
IssueLiteSerializer,
|
IssueLiteSerializer,
|
||||||
IssueAttachmentSerializer,
|
IssueAttachmentSerializer,
|
||||||
|
IssueSubscriberSerializer,
|
||||||
|
ProjectMemberLiteSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
WorkSpaceAdminPermission,
|
WorkSpaceAdminPermission,
|
||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Project,
|
Project,
|
||||||
@ -59,6 +63,8 @@ from plane.db.models import (
|
|||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
State,
|
State,
|
||||||
|
IssueSubscriber,
|
||||||
|
ProjectMember,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
@ -162,8 +168,8 @@ class IssueViewSet(BaseViewSet):
|
|||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.annotate(cycle_id=F("issue_cycle__id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__id"))
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -256,7 +262,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
print(e)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -905,3 +911,347 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueArchiveViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
serializer_class = IssueFlatSerializer
|
||||||
|
model = Issue
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
Issue.objects.annotate(
|
||||||
|
sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.filter(archived_at__isnull=False)
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("state")
|
||||||
|
.select_related("parent")
|
||||||
|
.prefetch_related("assignees")
|
||||||
|
.prefetch_related("labels")
|
||||||
|
)
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||||
|
|
||||||
|
# Custom ordering for priority and state
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", None]
|
||||||
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
|
|
||||||
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
|
issue_queryset = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(**filters)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
|
priority_order = (
|
||||||
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("priority_order")
|
||||||
|
|
||||||
|
# State Ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"state__name",
|
||||||
|
"state__group",
|
||||||
|
"-state__name",
|
||||||
|
"-state__group",
|
||||||
|
]:
|
||||||
|
state_order = (
|
||||||
|
state_order
|
||||||
|
if order_by_param in ["state__name", "state__group"]
|
||||||
|
else state_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
state_order=Case(
|
||||||
|
*[
|
||||||
|
When(state__group=state_group, then=Value(i))
|
||||||
|
for i, state_group in enumerate(state_order)
|
||||||
|
],
|
||||||
|
default=Value(len(state_order)),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("state_order")
|
||||||
|
# assignee and label ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"labels__name",
|
||||||
|
"-labels__name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"-assignees__first_name",
|
||||||
|
]:
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
max_values=Max(
|
||||||
|
order_by_param[1::]
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else order_by_param
|
||||||
|
)
|
||||||
|
).order_by(
|
||||||
|
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
|
issue_queryset = (
|
||||||
|
issue_queryset
|
||||||
|
if show_sub_issues == "true"
|
||||||
|
else issue_queryset.filter(parent__isnull=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||||
|
|
||||||
|
## Grouping the results
|
||||||
|
group_by = request.GET.get("group_by", False)
|
||||||
|
if group_by:
|
||||||
|
return Response(
|
||||||
|
group_results(issues, group_by), status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
|
try:
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
archived_at__isnull=False,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
|
except Issue.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def unarchive(self, request, slug, project_id, pk=None):
|
||||||
|
try:
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
archived_at__isnull=False,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
issue.archived_at = None
|
||||||
|
issue.save()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.updated",
|
||||||
|
requested_data=json.dumps({"archived_at": None}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue.id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
|
except Issue.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong, please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
class IssueSubscriberViewSet(BaseViewSet):
|
||||||
|
serializer_class = IssueSubscriberSerializer
|
||||||
|
model = IssueSubscriber
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action in ["subscribe", "unsubscribe", "subscription_status"]:
|
||||||
|
self.permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
return super(IssueSubscriberViewSet, self).get_permissions()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
issue_id=self.kwargs.get("issue_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||||
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
members = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
).annotate(
|
||||||
|
is_subscribed=Exists(
|
||||||
|
IssueSubscriber.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
subscriber=OuterRef("member"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).select_related("member")
|
||||||
|
serializer = ProjectMemberLiteSerializer(members, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": e},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, issue_id, subscriber_id):
|
||||||
|
try:
|
||||||
|
issue_subscriber = IssueSubscriber.objects.get(
|
||||||
|
project=project_id,
|
||||||
|
subscriber=subscriber_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
issue=issue_id,
|
||||||
|
)
|
||||||
|
issue_subscriber.delete()
|
||||||
|
return Response(
|
||||||
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
except IssueSubscriber.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "User is not subscribed to this issue"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscribe(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
if IssueSubscriber.objects.filter(
|
||||||
|
issue_id=issue_id,
|
||||||
|
subscriber=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project=project_id,
|
||||||
|
).exists():
|
||||||
|
return Response(
|
||||||
|
{"message": "User already subscribed to the issue."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
subscriber = IssueSubscriber.objects.create(
|
||||||
|
issue_id=issue_id,
|
||||||
|
subscriber_id=request.user.id,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
serilaizer = IssueSubscriberSerializer(subscriber)
|
||||||
|
return Response(serilaizer.data, status=status.HTTP_201_CREATED)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong, please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def unsubscribe(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
issue_subscriber = IssueSubscriber.objects.get(
|
||||||
|
project=project_id,
|
||||||
|
subscriber=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
issue=issue_id,
|
||||||
|
)
|
||||||
|
issue_subscriber.delete()
|
||||||
|
return Response(
|
||||||
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
except IssueSubscriber.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "User subscribed to this issue"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscription_status(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
issue_subscriber = IssueSubscriber.objects.filter(
|
||||||
|
issue=issue_id,
|
||||||
|
subscriber=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project=project_id,
|
||||||
|
).exists()
|
||||||
|
return Response({"subscribed": issue_subscriber}, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong, please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
@ -480,9 +480,6 @@ class ModuleLinkViewSet(BaseViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleFavoriteViewSet(BaseViewSet):
|
class ModuleFavoriteViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
serializer_class = ModuleFavoriteSerializer
|
serializer_class = ModuleFavoriteSerializer
|
||||||
model = ModuleFavorite
|
model = ModuleFavorite
|
||||||
|
258
apiserver/plane/api/views/notification.py
Normal file
258
apiserver/plane/api/views/notification.py
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .base import BaseViewSet, BaseAPIView
|
||||||
|
from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue
|
||||||
|
from plane.api.serializers import NotificationSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationViewSet(BaseViewSet):
|
||||||
|
model = Notification
|
||||||
|
serializer_class = NotificationSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
receiver_id=self.request.user.id,
|
||||||
|
)
|
||||||
|
.select_related("workspace", "project," "triggered_by", "receiver")
|
||||||
|
)
|
||||||
|
|
||||||
|
def list(self, request, slug):
|
||||||
|
try:
|
||||||
|
snoozed = request.GET.get("snoozed", "false")
|
||||||
|
archived = request.GET.get("archived", "false")
|
||||||
|
read = request.GET.get("read", "true")
|
||||||
|
|
||||||
|
# Filter type
|
||||||
|
type = request.GET.get("type", "all")
|
||||||
|
|
||||||
|
notifications = Notification.objects.filter(
|
||||||
|
workspace__slug=slug, receiver_id=request.user.id
|
||||||
|
).order_by("snoozed_till", "-created_at")
|
||||||
|
|
||||||
|
# Filter for snoozed notifications
|
||||||
|
if snoozed == "false":
|
||||||
|
notifications = notifications.filter(
|
||||||
|
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
if snoozed == "true":
|
||||||
|
notifications = notifications.filter(
|
||||||
|
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
if read == "false":
|
||||||
|
notifications = notifications.filter(read_at__isnull=True)
|
||||||
|
|
||||||
|
# Filter for archived or unarchive
|
||||||
|
if archived == "false":
|
||||||
|
notifications = notifications.filter(archived_at__isnull=True)
|
||||||
|
|
||||||
|
if archived == "true":
|
||||||
|
notifications = notifications.filter(archived_at__isnull=False)
|
||||||
|
|
||||||
|
# Subscribed issues
|
||||||
|
if type == "watching":
|
||||||
|
issue_ids = IssueSubscriber.objects.filter(
|
||||||
|
workspace__slug=slug, subscriber_id=request.user.id
|
||||||
|
).values_list("issue_id", flat=True)
|
||||||
|
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||||
|
|
||||||
|
# Assigned Issues
|
||||||
|
if type == "assigned":
|
||||||
|
issue_ids = IssueAssignee.objects.filter(
|
||||||
|
workspace__slug=slug, assignee_id=request.user.id
|
||||||
|
).values_list("issue_id", flat=True)
|
||||||
|
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||||
|
|
||||||
|
# Created issues
|
||||||
|
if type == "created":
|
||||||
|
issue_ids = Issue.objects.filter(
|
||||||
|
workspace__slug=slug, created_by=request.user
|
||||||
|
).values_list("pk", flat=True)
|
||||||
|
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||||
|
|
||||||
|
serializer = NotificationSerializer(notifications, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, pk):
|
||||||
|
try:
|
||||||
|
notification = Notification.objects.get(
|
||||||
|
workspace__slug=slug, pk=pk, receiver=request.user
|
||||||
|
)
|
||||||
|
# Only read_at and snoozed_till can be updated
|
||||||
|
notification_data = {
|
||||||
|
"snoozed_till": request.data.get("snoozed_till", None),
|
||||||
|
}
|
||||||
|
serializer = NotificationSerializer(
|
||||||
|
notification, data=notification_data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except Notification.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Notification 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def mark_read(self, request, slug, pk):
|
||||||
|
try:
|
||||||
|
notification = Notification.objects.get(
|
||||||
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
|
)
|
||||||
|
notification.read_at = timezone.now()
|
||||||
|
notification.save()
|
||||||
|
serializer = NotificationSerializer(notification)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Notification.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Notification 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def mark_unread(self, request, slug, pk):
|
||||||
|
try:
|
||||||
|
notification = Notification.objects.get(
|
||||||
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
|
)
|
||||||
|
notification.read_at = None
|
||||||
|
notification.save()
|
||||||
|
serializer = NotificationSerializer(notification)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Notification.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Notification 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def archive(self, request, slug, pk):
|
||||||
|
try:
|
||||||
|
notification = Notification.objects.get(
|
||||||
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
|
)
|
||||||
|
notification.archived_at = timezone.now()
|
||||||
|
notification.save()
|
||||||
|
serializer = NotificationSerializer(notification)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Notification.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Notification 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def unarchive(self, request, slug, pk):
|
||||||
|
try:
|
||||||
|
notification = Notification.objects.get(
|
||||||
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
|
)
|
||||||
|
notification.archived_at = None
|
||||||
|
notification.save()
|
||||||
|
serializer = NotificationSerializer(notification)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Notification.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Notification 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 UnreadNotificationEndpoint(BaseAPIView):
|
||||||
|
def get(self, request, slug):
|
||||||
|
try:
|
||||||
|
# Watching Issues Count
|
||||||
|
watching_issues_count = Notification.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
receiver_id=request.user.id,
|
||||||
|
read_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
entity_identifier__in=IssueSubscriber.objects.filter(
|
||||||
|
workspace__slug=slug, subscriber_id=request.user.id
|
||||||
|
).values_list("issue_id", flat=True),
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# My Issues Count
|
||||||
|
my_issues_count = Notification.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
receiver_id=request.user.id,
|
||||||
|
read_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
entity_identifier__in=IssueAssignee.objects.filter(
|
||||||
|
workspace__slug=slug, assignee_id=request.user.id
|
||||||
|
).values_list("issue_id", flat=True),
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Created Issues Count
|
||||||
|
created_issues_count = Notification.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
receiver_id=request.user.id,
|
||||||
|
read_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
entity_identifier__in=Issue.objects.filter(
|
||||||
|
workspace__slug=slug, created_by=request.user
|
||||||
|
).values_list("pk", flat=True),
|
||||||
|
).count()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"watching_issues": watching_issues_count,
|
||||||
|
"my_issues": my_issues_count,
|
||||||
|
"created_issues": created_issues_count,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
@ -37,7 +37,9 @@ class UserEndpoint(BaseViewSet):
|
|||||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
email=request.user.email
|
email=request.user.email
|
||||||
).count()
|
).count()
|
||||||
assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count()
|
assigned_issues = Issue.issue_objects.filter(
|
||||||
|
assignees__in=[request.user]
|
||||||
|
).count()
|
||||||
|
|
||||||
serialized_data = UserSerializer(request.user).data
|
serialized_data = UserSerializer(request.user).data
|
||||||
serialized_data["workspace"] = {
|
serialized_data["workspace"] = {
|
||||||
@ -47,7 +49,9 @@ class UserEndpoint(BaseViewSet):
|
|||||||
"fallback_workspace_slug": workspace.slug,
|
"fallback_workspace_slug": workspace.slug,
|
||||||
"invites": workspace_invites,
|
"invites": workspace_invites,
|
||||||
}
|
}
|
||||||
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues
|
serialized_data.setdefault("issues", {})[
|
||||||
|
"assigned_issues"
|
||||||
|
] = assigned_issues
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
serialized_data,
|
serialized_data,
|
||||||
@ -59,11 +63,15 @@ class UserEndpoint(BaseViewSet):
|
|||||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
email=request.user.email
|
email=request.user.email
|
||||||
).count()
|
).count()
|
||||||
assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count()
|
assigned_issues = Issue.issue_objects.filter(
|
||||||
|
assignees__in=[request.user]
|
||||||
|
).count()
|
||||||
|
|
||||||
fallback_workspace = Workspace.objects.filter(
|
fallback_workspace = (
|
||||||
workspace_member__member=request.user
|
Workspace.objects.filter(workspace_member__member=request.user)
|
||||||
).order_by("created_at").first()
|
.order_by("created_at")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
serialized_data = UserSerializer(request.user).data
|
serialized_data = UserSerializer(request.user).data
|
||||||
|
|
||||||
@ -78,7 +86,9 @@ class UserEndpoint(BaseViewSet):
|
|||||||
else None,
|
else None,
|
||||||
"invites": workspace_invites,
|
"invites": workspace_invites,
|
||||||
}
|
}
|
||||||
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues
|
serialized_data.setdefault("issues", {})[
|
||||||
|
"assigned_issues"
|
||||||
|
] = assigned_issues
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
serialized_data,
|
serialized_data,
|
||||||
@ -109,6 +119,23 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||||
|
def patch(self, request):
|
||||||
|
try:
|
||||||
|
user = User.objects.get(pk=request.user.id)
|
||||||
|
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
||||||
|
user.save()
|
||||||
|
return Response(
|
||||||
|
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
try:
|
try:
|
||||||
|
@ -96,6 +96,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
try:
|
try:
|
||||||
|
is_favorite = request.GET.get("is_favorite", "all")
|
||||||
subquery = ProjectFavorite.objects.filter(
|
subquery = ProjectFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
@ -126,6 +127,12 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if is_favorite == "true":
|
||||||
|
projects = projects.filter(is_favorite=True)
|
||||||
|
if is_favorite == "false":
|
||||||
|
projects = projects.filter(is_favorite=False)
|
||||||
|
|
||||||
return Response(ProjectDetailSerializer(projects, many=True).data)
|
return Response(ProjectDetailSerializer(projects, many=True).data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
@ -153,32 +160,32 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
states = [
|
states = [
|
||||||
{
|
{
|
||||||
"name": "Backlog",
|
"name": "Backlog",
|
||||||
"color": "#5e6ad2",
|
"color": "#A3A3A3",
|
||||||
"sequence": 15000,
|
"sequence": 15000,
|
||||||
"group": "backlog",
|
"group": "backlog",
|
||||||
"default": True,
|
"default": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Todo",
|
"name": "Todo",
|
||||||
"color": "#eb5757",
|
"color": "#3A3A3A",
|
||||||
"sequence": 25000,
|
"sequence": 25000,
|
||||||
"group": "unstarted",
|
"group": "unstarted",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "In Progress",
|
"name": "In Progress",
|
||||||
"color": "#26b5ce",
|
"color": "#F59E0B",
|
||||||
"sequence": 35000,
|
"sequence": 35000,
|
||||||
"group": "started",
|
"group": "started",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Done",
|
"name": "Done",
|
||||||
"color": "#f2c94c",
|
"color": "#16A34A",
|
||||||
"sequence": 45000,
|
"sequence": 45000,
|
||||||
"group": "completed",
|
"group": "completed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Cancelled",
|
"name": "Cancelled",
|
||||||
"color": "#4cb782",
|
"color": "#EF4444",
|
||||||
"sequence": 55000,
|
"sequence": 55000,
|
||||||
"group": "cancelled",
|
"group": "cancelled",
|
||||||
},
|
},
|
||||||
@ -259,7 +266,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
group="backlog",
|
group="backlog",
|
||||||
description="Default state for managing all Inbox Issues",
|
description="Default state for managing all Inbox Issues",
|
||||||
project_id=pk,
|
project_id=pk,
|
||||||
color="#ff7700"
|
color="#ff7700",
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -550,45 +557,47 @@ class AddMemberToProjectEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
member_id = request.data.get("member_id", False)
|
members = request.data.get("members", [])
|
||||||
role = request.data.get("role", False)
|
|
||||||
|
|
||||||
if not member_id or not role:
|
# get the project
|
||||||
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
|
|
||||||
|
if not len(members):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Member ID and role is required"},
|
{"error": "Atleast one member is required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if the user is a member in the workspace
|
project_members = ProjectMember.objects.bulk_create(
|
||||||
if not WorkspaceMember.objects.filter(
|
[
|
||||||
workspace__slug=slug, member_id=member_id
|
ProjectMember(
|
||||||
).exists():
|
member_id=member.get("member_id"),
|
||||||
# TODO: Update this error message - nk
|
role=member.get("role", 10),
|
||||||
return Response(
|
project_id=project_id,
|
||||||
{
|
workspace_id=project.workspace_id,
|
||||||
"error": "User is not a member of the workspace. Invite the user to the workspace to add him to project"
|
)
|
||||||
},
|
for member in members
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
],
|
||||||
)
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
# Check if the user is already member of project
|
|
||||||
if ProjectMember.objects.filter(
|
|
||||||
project=project_id, member_id=member_id
|
|
||||||
).exists():
|
|
||||||
return Response(
|
|
||||||
{"error": "User is already a member of the project"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the user to project
|
|
||||||
project_member = ProjectMember.objects.create(
|
|
||||||
project_id=project_id, member_id=member_id, role=role
|
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = ProjectMemberSerializer(project_member)
|
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
except KeyError:
|
||||||
|
return Response(
|
||||||
|
{"error": "Incorrect data sent"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
except Project.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Project does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
except IntegrityError:
|
||||||
|
return Response(
|
||||||
|
{"error": "User not member of the workspace"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -3,6 +3,7 @@ import jwt
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
@ -94,14 +95,34 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
return self.filter_queryset(
|
return (
|
||||||
super().get_queryset().select_related("owner")
|
self.filter_queryset(super().get_queryset().select_related("owner"))
|
||||||
).order_by("name").filter(workspace_member__member=self.request.user).annotate(total_members=member_count).annotate(total_issues=issue_count)
|
.order_by("name")
|
||||||
|
.filter(workspace_member__member=self.request.user)
|
||||||
|
.annotate(total_members=member_count)
|
||||||
|
.annotate(total_issues=issue_count)
|
||||||
|
.select_related("owner")
|
||||||
|
)
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
try:
|
try:
|
||||||
serializer = WorkSpaceSerializer(data=request.data)
|
serializer = WorkSpaceSerializer(data=request.data)
|
||||||
|
|
||||||
|
slug = request.data.get("slug", False)
|
||||||
|
name = request.data.get("name", False)
|
||||||
|
|
||||||
|
if not name or not slug:
|
||||||
|
return Response(
|
||||||
|
{"error": "Both name and slug are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(name) > 80 or len(slug) > 48:
|
||||||
|
return Response(
|
||||||
|
{"error": "The maximum length for name is 80 and for slug is 48"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(owner=request.user)
|
serializer.save(owner=request.user)
|
||||||
# Create Workspace member
|
# Create Workspace member
|
||||||
@ -161,14 +182,20 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
workspace = (
|
workspace = (
|
||||||
Workspace.objects.prefetch_related(
|
(
|
||||||
Prefetch("workspace_member", queryset=WorkspaceMember.objects.all())
|
Workspace.objects.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"workspace_member", queryset=WorkspaceMember.objects.all()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
workspace_member__member=request.user,
|
||||||
|
)
|
||||||
|
.select_related("owner")
|
||||||
)
|
)
|
||||||
.filter(
|
.annotate(total_members=member_count)
|
||||||
workspace_member__member=request.user,
|
.annotate(total_issues=issue_count)
|
||||||
)
|
)
|
||||||
.select_related("owner")
|
|
||||||
).annotate(total_members=member_count).annotate(total_issues=issue_count)
|
|
||||||
|
|
||||||
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
|
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -217,9 +244,20 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# check for role level
|
# check for role level
|
||||||
requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user)
|
requesting_user = WorkspaceMember.objects.get(
|
||||||
if len([email for email in emails if int(email.get("role", 10)) > requesting_user.role]):
|
workspace__slug=slug, member=request.user
|
||||||
return Response({"error": "You cannot invite a user with higher role"}, status=status.HTTP_400_BAD_REQUEST)
|
)
|
||||||
|
if len(
|
||||||
|
[
|
||||||
|
email
|
||||||
|
for email in emails
|
||||||
|
if int(email.get("role", 10)) > requesting_user.role
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot invite a user with higher role"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
@ -894,7 +932,9 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
state_distribution = (
|
state_distribution = (
|
||||||
Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user])
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, assignees__in=[request.user]
|
||||||
|
)
|
||||||
.annotate(state_group=F("state__group"))
|
.annotate(state_group=F("state__group"))
|
||||||
.values("state_group")
|
.values("state_group")
|
||||||
.annotate(state_count=Count("state_group"))
|
.annotate(state_count=Count("state_group"))
|
||||||
|
@ -5,6 +5,7 @@ import requests
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
@ -20,6 +21,9 @@ from plane.db.models import (
|
|||||||
State,
|
State,
|
||||||
Cycle,
|
Cycle,
|
||||||
Module,
|
Module,
|
||||||
|
IssueSubscriber,
|
||||||
|
Notification,
|
||||||
|
IssueAssignee,
|
||||||
)
|
)
|
||||||
from plane.api.serializers import IssueActivitySerializer
|
from plane.api.serializers import IssueActivitySerializer
|
||||||
|
|
||||||
@ -554,6 +558,64 @@ def track_estimate_points(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def track_archive_at(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
if requested_data.get("archived_at") is None:
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"{actor.email} has restored the issue",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
field="archived_at",
|
||||||
|
old_value="archive",
|
||||||
|
new_value="restore",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"Plane has archived the issue",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
field="archived_at",
|
||||||
|
old_value=None,
|
||||||
|
new_value="archive",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def track_closed_to(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
if requested_data.get("closed_to") is not None:
|
||||||
|
updated_state = State.objects.get(
|
||||||
|
pk=requested_data.get("closed_to"), project=project
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
actor=actor,
|
||||||
|
verb="updated",
|
||||||
|
old_value=None,
|
||||||
|
new_value=updated_state.name,
|
||||||
|
field="state",
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"Plane updated the state to {updated_state.name}",
|
||||||
|
old_identifier=None,
|
||||||
|
new_identifier=updated_state.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_issue_activity(
|
def update_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
):
|
):
|
||||||
@ -570,6 +632,8 @@ def update_issue_activity(
|
|||||||
"blocks_list": track_blocks,
|
"blocks_list": track_blocks,
|
||||||
"blockers_list": track_blockings,
|
"blockers_list": track_blockings,
|
||||||
"estimate_point": track_estimate_points,
|
"estimate_point": track_estimate_points,
|
||||||
|
"archived_at": track_archive_at,
|
||||||
|
"closed_to": track_closed_to,
|
||||||
}
|
}
|
||||||
|
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
@ -950,7 +1014,13 @@ def delete_attachment_activity(
|
|||||||
# Receive message from room group
|
# Receive message from room group
|
||||||
@shared_task
|
@shared_task
|
||||||
def issue_activity(
|
def issue_activity(
|
||||||
type, requested_data, current_instance, issue_id, actor_id, project_id
|
type,
|
||||||
|
requested_data,
|
||||||
|
current_instance,
|
||||||
|
issue_id,
|
||||||
|
actor_id,
|
||||||
|
project_id,
|
||||||
|
subscriber=True,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
issue_activities = []
|
issue_activities = []
|
||||||
@ -958,6 +1028,27 @@ def issue_activity(
|
|||||||
actor = User.objects.get(pk=actor_id)
|
actor = User.objects.get(pk=actor_id)
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
if type not in [
|
||||||
|
"cycle.activity.created",
|
||||||
|
"cycle.activity.deleted",
|
||||||
|
"module.activity.created",
|
||||||
|
"module.activity.deleted",
|
||||||
|
]:
|
||||||
|
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
|
||||||
|
|
||||||
|
if issue is not None:
|
||||||
|
issue.updated_at = timezone.now()
|
||||||
|
issue.save(update_fields=["updated_at"])
|
||||||
|
|
||||||
|
if subscriber:
|
||||||
|
# add the user to issue subscriber
|
||||||
|
try:
|
||||||
|
_ = IssueSubscriber.objects.get_or_create(
|
||||||
|
issue_id=issue_id, subscriber=actor
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
ACTIVITY_MAPPER = {
|
ACTIVITY_MAPPER = {
|
||||||
"issue.activity.created": create_issue_activity,
|
"issue.activity.created": create_issue_activity,
|
||||||
"issue.activity.updated": update_issue_activity,
|
"issue.activity.updated": update_issue_activity,
|
||||||
@ -992,18 +1083,97 @@ def issue_activity(
|
|||||||
# Post the updates to segway for integrations and webhooks
|
# Post the updates to segway for integrations and webhooks
|
||||||
if len(issue_activities_created):
|
if len(issue_activities_created):
|
||||||
# Don't send activities if the actor is a bot
|
# Don't send activities if the actor is a bot
|
||||||
if settings.PROXY_BASE_URL:
|
try:
|
||||||
|
if settings.PROXY_BASE_URL:
|
||||||
|
for issue_activity in issue_activities_created:
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
issue_activity_json = json.dumps(
|
||||||
|
IssueActivitySerializer(issue_activity).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
)
|
||||||
|
_ = requests.post(
|
||||||
|
f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(issue_activity.workspace_id)}/projects/{str(issue_activity.project_id)}/issues/{str(issue_activity.issue_id)}/issue-activity-hooks/",
|
||||||
|
json=issue_activity_json,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
|
if type not in [
|
||||||
|
"cycle.activity.created",
|
||||||
|
"cycle.activity.deleted",
|
||||||
|
"module.activity.created",
|
||||||
|
"module.activity.deleted",
|
||||||
|
]:
|
||||||
|
# Create Notifications
|
||||||
|
bulk_notifications = []
|
||||||
|
|
||||||
|
issue_subscribers = list(
|
||||||
|
IssueSubscriber.objects.filter(project=project, issue_id=issue_id)
|
||||||
|
.exclude(subscriber_id=actor_id)
|
||||||
|
.values_list("subscriber", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_assignees = list(
|
||||||
|
IssueAssignee.objects.filter(project=project, issue_id=issue_id)
|
||||||
|
.exclude(assignee_id=actor_id)
|
||||||
|
.values_list("assignee", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_subscribers = issue_subscribers + issue_assignees
|
||||||
|
|
||||||
|
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
|
||||||
|
|
||||||
|
# Add bot filtering
|
||||||
|
if (
|
||||||
|
issue is not None
|
||||||
|
and issue.created_by_id is not None
|
||||||
|
and not issue.created_by.is_bot
|
||||||
|
and str(issue.created_by_id) != str(actor_id)
|
||||||
|
):
|
||||||
|
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
||||||
|
|
||||||
|
for subscriber in issue_subscribers:
|
||||||
for issue_activity in issue_activities_created:
|
for issue_activity in issue_activities_created:
|
||||||
headers = {"Content-Type": "application/json"}
|
bulk_notifications.append(
|
||||||
issue_activity_json = json.dumps(
|
Notification(
|
||||||
IssueActivitySerializer(issue_activity).data,
|
workspace=project.workspace,
|
||||||
cls=DjangoJSONEncoder,
|
sender="in_app:issue_activities",
|
||||||
)
|
triggered_by_id=actor_id,
|
||||||
_ = requests.post(
|
receiver_id=subscriber,
|
||||||
f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(issue_activity.workspace_id)}/projects/{str(issue_activity.project_id)}/issues/{str(issue_activity.issue_id)}/issue-activity-hooks/",
|
entity_identifier=issue_id,
|
||||||
json=issue_activity_json,
|
entity_name="issue",
|
||||||
headers=headers,
|
project=project,
|
||||||
|
title=issue_activity.comment,
|
||||||
|
data={
|
||||||
|
"issue": {
|
||||||
|
"id": str(issue_id),
|
||||||
|
"name": str(issue.name),
|
||||||
|
"identifier": str(project.identifier),
|
||||||
|
"sequence_id": issue.sequence_id,
|
||||||
|
"state_name": issue.state.name,
|
||||||
|
"state_group": issue.state.group,
|
||||||
|
},
|
||||||
|
"issue_activity": {
|
||||||
|
"id": str(issue_activity.id),
|
||||||
|
"verb": str(issue_activity.verb),
|
||||||
|
"field": str(issue_activity.field),
|
||||||
|
"actor": str(issue_activity.actor_id),
|
||||||
|
"new_value": str(issue_activity.new_value),
|
||||||
|
"old_value": str(issue_activity.old_value),
|
||||||
|
"issue_comment": str(
|
||||||
|
issue_activity.issue_comment.comment_stripped
|
||||||
|
if issue_activity.issue_comment is not None
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Bulk create notifications
|
||||||
|
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
|
||||||
|
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Print logs if in DEBUG mode
|
# Print logs if in DEBUG mode
|
||||||
|
157
apiserver/plane/bgtasks/issue_automation_task.py
Normal file
157
apiserver/plane/bgtasks/issue_automation_task.py
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from celery import shared_task
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import Issue, Project, State
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def archive_and_close_old_issues():
|
||||||
|
archive_old_issues()
|
||||||
|
close_old_issues()
|
||||||
|
|
||||||
|
|
||||||
|
def archive_old_issues():
|
||||||
|
try:
|
||||||
|
# Get all the projects whose archive_in is greater than 0
|
||||||
|
projects = Project.objects.filter(archive_in__gt=0)
|
||||||
|
|
||||||
|
for project in projects:
|
||||||
|
project_id = project.id
|
||||||
|
archive_in = project.archive_in
|
||||||
|
|
||||||
|
# Get all the issues whose updated_at in less that the archive_in month
|
||||||
|
issues = Issue.objects.filter(
|
||||||
|
Q(
|
||||||
|
project=project_id,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
updated_at__lte=(timezone.now() - timedelta(days=archive_in * 30)),
|
||||||
|
state__group__in=["completed", "cancelled"],
|
||||||
|
),
|
||||||
|
Q(issue_cycle__isnull=True)
|
||||||
|
| (
|
||||||
|
Q(issue_cycle__cycle__end_date__lt=timezone.now().date())
|
||||||
|
& Q(issue_cycle__isnull=False)
|
||||||
|
),
|
||||||
|
Q(issue_module__isnull=True)
|
||||||
|
| (
|
||||||
|
Q(issue_module__module__target_date__lt=timezone.now().date())
|
||||||
|
& Q(issue_module__isnull=False)
|
||||||
|
),
|
||||||
|
).filter(
|
||||||
|
Q(issue_inbox__status=1)
|
||||||
|
| Q(issue_inbox__status=-1)
|
||||||
|
| Q(issue_inbox__status=2)
|
||||||
|
| Q(issue_inbox__isnull=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if Issues
|
||||||
|
if issues:
|
||||||
|
issues_to_update = []
|
||||||
|
for issue in issues:
|
||||||
|
issue.archived_at = timezone.now()
|
||||||
|
issues_to_update.append(issue)
|
||||||
|
|
||||||
|
# Bulk Update the issues and log the activity
|
||||||
|
Issue.objects.bulk_update(
|
||||||
|
issues_to_update, ["archived_at"], batch_size=100
|
||||||
|
)
|
||||||
|
[
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.updated",
|
||||||
|
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
|
||||||
|
actor_id=str(project.created_by_id),
|
||||||
|
issue_id=issue.id,
|
||||||
|
project_id=project_id,
|
||||||
|
current_instance=None,
|
||||||
|
subscriber=False,
|
||||||
|
)
|
||||||
|
for issue in issues_to_update
|
||||||
|
]
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
capture_exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def close_old_issues():
|
||||||
|
try:
|
||||||
|
# Get all the projects whose close_in is greater than 0
|
||||||
|
projects = Project.objects.filter(close_in__gt=0).select_related(
|
||||||
|
"default_state"
|
||||||
|
)
|
||||||
|
|
||||||
|
for project in projects:
|
||||||
|
project_id = project.id
|
||||||
|
close_in = project.close_in
|
||||||
|
|
||||||
|
# Get all the issues whose updated_at in less that the close_in month
|
||||||
|
issues = Issue.objects.filter(
|
||||||
|
Q(
|
||||||
|
project=project_id,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
updated_at__lte=(timezone.now() - timedelta(days=close_in * 30)),
|
||||||
|
state__group__in=["backlog", "unstarted", "started"],
|
||||||
|
),
|
||||||
|
Q(issue_cycle__isnull=True)
|
||||||
|
| (
|
||||||
|
Q(issue_cycle__cycle__end_date__lt=timezone.now().date())
|
||||||
|
& Q(issue_cycle__isnull=False)
|
||||||
|
),
|
||||||
|
Q(issue_module__isnull=True)
|
||||||
|
| (
|
||||||
|
Q(issue_module__module__target_date__lt=timezone.now().date())
|
||||||
|
& Q(issue_module__isnull=False)
|
||||||
|
),
|
||||||
|
).filter(
|
||||||
|
Q(issue_inbox__status=1)
|
||||||
|
| Q(issue_inbox__status=-1)
|
||||||
|
| Q(issue_inbox__status=2)
|
||||||
|
| Q(issue_inbox__isnull=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if Issues
|
||||||
|
if issues:
|
||||||
|
if project.default_state is None:
|
||||||
|
close_state = State.objects.filter(group="cancelled").first()
|
||||||
|
else:
|
||||||
|
close_state = project.default_state
|
||||||
|
|
||||||
|
issues_to_update = []
|
||||||
|
for issue in issues:
|
||||||
|
issue.state = close_state
|
||||||
|
issues_to_update.append(issue)
|
||||||
|
|
||||||
|
# Bulk Update the issues and log the activity
|
||||||
|
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||||
|
[
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.updated",
|
||||||
|
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
|
||||||
|
actor_id=str(project.created_by_id),
|
||||||
|
issue_id=issue.id,
|
||||||
|
project_id=project_id,
|
||||||
|
current_instance=None,
|
||||||
|
subscriber=False,
|
||||||
|
)
|
||||||
|
for issue in issues_to_update
|
||||||
|
]
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
capture_exception(e)
|
||||||
|
return
|
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
|
from celery.schedules import crontab
|
||||||
|
|
||||||
# Set the default Django settings module for the 'celery' program.
|
# Set the default Django settings module for the 'celery' program.
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
|
||||||
@ -13,5 +14,15 @@ app = Celery("plane")
|
|||||||
# pickle the object when using Windows.
|
# pickle the object when using Windows.
|
||||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
|
||||||
|
app.conf.beat_schedule = {
|
||||||
|
# Executes every day at 12 AM
|
||||||
|
"check-every-day-to-archive-and-close": {
|
||||||
|
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
|
||||||
|
"schedule": crontab(hour=0, minute=0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# Load task modules from all registered Django app configs.
|
# Load task modules from all registered Django app configs.
|
||||||
app.autodiscover_tasks()
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
|
app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler'
|
42
apiserver/plane/db/migrations/0035_auto_20230704_2225.py
Normal file
42
apiserver/plane/db/migrations/0035_auto_20230704_2225.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-07-04 16:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def update_company_organization_size(apps, schema_editor):
|
||||||
|
Model = apps.get_model("db", "Workspace")
|
||||||
|
updated_size = []
|
||||||
|
for obj in Model.objects.all():
|
||||||
|
obj.organization_size = str(obj.company_size)
|
||||||
|
updated_size.append(obj)
|
||||||
|
|
||||||
|
Model.objects.bulk_update(updated_size, ["organization_size"], batch_size=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("db", "0034_auto_20230628_1046"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workspace",
|
||||||
|
name="organization_size",
|
||||||
|
field=models.CharField(default="2-10", max_length=20),
|
||||||
|
),
|
||||||
|
migrations.RunPython(update_company_organization_size),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workspace",
|
||||||
|
name="name",
|
||||||
|
field=models.CharField(max_length=80, verbose_name="Workspace Name"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workspace",
|
||||||
|
name="slug",
|
||||||
|
field=models.SlugField(max_length=48, unique=True),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="workspace",
|
||||||
|
name="company_size",
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-07-05 07:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0035_auto_20230704_2225'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspace',
|
||||||
|
name='organization_size',
|
||||||
|
field=models.CharField(max_length=20),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,266 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-07-19 06:52
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import plane.db.models.user
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def onboarding_default_steps(apps, schema_editor):
|
||||||
|
default_onboarding_schema = {
|
||||||
|
"workspace_join": True,
|
||||||
|
"profile_complete": True,
|
||||||
|
"workspace_create": True,
|
||||||
|
"workspace_invite": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = apps.get_model("db", "User")
|
||||||
|
updated_user = []
|
||||||
|
for obj in Model.objects.filter(is_onboarded=True):
|
||||||
|
obj.onboarding_step = default_onboarding_schema
|
||||||
|
obj.is_tour_completed = True
|
||||||
|
updated_user.append(obj)
|
||||||
|
|
||||||
|
Model.objects.bulk_update(updated_user, ["onboarding_step", "is_tour_completed"], batch_size=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("db", "0036_alter_workspace_organization_size"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="issue",
|
||||||
|
name="archived_at",
|
||||||
|
field=models.DateField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="project",
|
||||||
|
name="archive_in",
|
||||||
|
field=models.IntegerField(
|
||||||
|
default=0,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(0),
|
||||||
|
django.core.validators.MaxValueValidator(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="project",
|
||||||
|
name="close_in",
|
||||||
|
field=models.IntegerField(
|
||||||
|
default=0,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(0),
|
||||||
|
django.core.validators.MaxValueValidator(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="project",
|
||||||
|
name="default_state",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="default_state",
|
||||||
|
to="db.state",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_tour_completed",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="onboarding_step",
|
||||||
|
field=models.JSONField(default=plane.db.models.user.get_default_onboarding),
|
||||||
|
),
|
||||||
|
migrations.RunPython(onboarding_default_steps),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Notification",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now=True, verbose_name="Last Modified At"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
db_index=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("data", models.JSONField(null=True)),
|
||||||
|
("entity_identifier", models.UUIDField(null=True)),
|
||||||
|
("entity_name", models.CharField(max_length=255)),
|
||||||
|
("title", models.TextField()),
|
||||||
|
("message", models.JSONField(null=True)),
|
||||||
|
("message_html", models.TextField(blank=True, default="<p></p>")),
|
||||||
|
("message_stripped", models.TextField(blank=True, null=True)),
|
||||||
|
("sender", models.CharField(max_length=255)),
|
||||||
|
("read_at", models.DateTimeField(null=True)),
|
||||||
|
("snoozed_till", models.DateTimeField(null=True)),
|
||||||
|
("archived_at", models.DateTimeField(null=True)),
|
||||||
|
(
|
||||||
|
"created_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="%(class)s_created_by",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Created By",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"project",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="notifications",
|
||||||
|
to="db.project",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"receiver",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="received_notifications",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"triggered_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="triggered_notifications",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="%(class)s_updated_by",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Last Modified By",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workspace",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="notifications",
|
||||||
|
to="db.workspace",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Notification",
|
||||||
|
"verbose_name_plural": "Notifications",
|
||||||
|
"db_table": "notifications",
|
||||||
|
"ordering": ("-created_at",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="IssueSubscriber",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now=True, verbose_name="Last Modified At"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
db_index=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="%(class)s_created_by",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Created By",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"issue",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="issue_subscribers",
|
||||||
|
to="db.issue",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"project",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="project_%(class)s",
|
||||||
|
to="db.project",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"subscriber",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="issue_subscribers",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="%(class)s_updated_by",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Last Modified By",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workspace",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="workspace_%(class)s",
|
||||||
|
to="db.workspace",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Issue Subscriber",
|
||||||
|
"verbose_name_plural": "Issue Subscribers",
|
||||||
|
"db_table": "issue_subscribers",
|
||||||
|
"ordering": ("-created_at",),
|
||||||
|
"unique_together": {("issue", "subscriber")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
35
apiserver/plane/db/migrations/0038_auto_20230720_1505.py
Normal file
35
apiserver/plane/db/migrations/0038_auto_20230720_1505.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-07-20 09:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def restructure_theming(apps, schema_editor):
|
||||||
|
Model = apps.get_model("db", "User")
|
||||||
|
updated_user = []
|
||||||
|
for obj in Model.objects.exclude(theme={}).all():
|
||||||
|
current_theme = obj.theme
|
||||||
|
updated_theme = {
|
||||||
|
"primary": current_theme.get("accent", ""),
|
||||||
|
"background": current_theme.get("bgBase", ""),
|
||||||
|
"sidebarBackground": current_theme.get("sidebar", ""),
|
||||||
|
"text": current_theme.get("textBase", ""),
|
||||||
|
"sidebarText": current_theme.get("textBase", ""),
|
||||||
|
"palette": f"""{current_theme.get("bgBase","")},{current_theme.get("textBase", "")},{current_theme.get("accent", "")},{current_theme.get("sidebar","")},{current_theme.get("textBase", "")}""",
|
||||||
|
"darkPalette": current_theme.get("darkPalette", "")
|
||||||
|
}
|
||||||
|
obj.theme = updated_theme
|
||||||
|
updated_user.append(obj)
|
||||||
|
|
||||||
|
Model.objects.bulk_update(
|
||||||
|
updated_user, ["theme"], batch_size=100
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("db", "0037_issue_archived_at_project_archive_in_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(restructure_theming)
|
||||||
|
]
|
@ -33,6 +33,7 @@ from .issue import (
|
|||||||
IssueLink,
|
IssueLink,
|
||||||
IssueSequence,
|
IssueSequence,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
IssueSubscriber,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .asset import FileAsset
|
from .asset import FileAsset
|
||||||
@ -66,4 +67,7 @@ from .page import Page, PageBlock, PageFavorite, PageLabel
|
|||||||
from .estimate import Estimate, EstimatePoint
|
from .estimate import Estimate, EstimatePoint
|
||||||
|
|
||||||
from .inbox import Inbox, InboxIssue
|
from .inbox import Inbox, InboxIssue
|
||||||
|
|
||||||
from .analytic import AnalyticView
|
from .analytic import AnalyticView
|
||||||
|
|
||||||
|
from .notification import Notification
|
@ -28,6 +28,7 @@ class IssueManager(models.Manager):
|
|||||||
| models.Q(issue_inbox__status=2)
|
| models.Q(issue_inbox__status=2)
|
||||||
| models.Q(issue_inbox__isnull=True)
|
| models.Q(issue_inbox__isnull=True)
|
||||||
)
|
)
|
||||||
|
.exclude(archived_at__isnull=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -81,6 +82,7 @@ class Issue(ProjectBaseModel):
|
|||||||
)
|
)
|
||||||
sort_order = models.FloatField(default=65535)
|
sort_order = models.FloatField(default=65535)
|
||||||
completed_at = models.DateTimeField(null=True)
|
completed_at = models.DateTimeField(null=True)
|
||||||
|
archived_at = models.DateField(null=True)
|
||||||
|
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
issue_objects = IssueManager()
|
issue_objects = IssueManager()
|
||||||
@ -401,6 +403,27 @@ class IssueSequence(ProjectBaseModel):
|
|||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueSubscriber(ProjectBaseModel):
|
||||||
|
issue = models.ForeignKey(
|
||||||
|
Issue, on_delete=models.CASCADE, related_name="issue_subscribers"
|
||||||
|
)
|
||||||
|
subscriber = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="issue_subscribers",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["issue", "subscriber"]
|
||||||
|
verbose_name = "Issue Subscriber"
|
||||||
|
verbose_name_plural = "Issue Subscribers"
|
||||||
|
db_table = "issue_subscribers"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.issue.name} {self.subscriber.email}"
|
||||||
|
|
||||||
|
|
||||||
# TODO: Find a better method to save the model
|
# TODO: Find a better method to save the model
|
||||||
@receiver(post_save, sender=Issue)
|
@receiver(post_save, sender=Issue)
|
||||||
def create_issue_sequence(sender, instance, created, **kwargs):
|
def create_issue_sequence(sender, instance, created, **kwargs):
|
||||||
|
37
apiserver/plane/db/models/notification.py
Normal file
37
apiserver/plane/db/models/notification.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from .base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Notification(BaseModel):
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.Workspace", related_name="notifications", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
project = models.ForeignKey(
|
||||||
|
"db.Project", related_name="notifications", on_delete=models.CASCADE, null=True
|
||||||
|
)
|
||||||
|
data = models.JSONField(null=True)
|
||||||
|
entity_identifier = models.UUIDField(null=True)
|
||||||
|
entity_name = models.CharField(max_length=255)
|
||||||
|
title = models.TextField()
|
||||||
|
message = models.JSONField(null=True)
|
||||||
|
message_html = models.TextField(blank=True, default="<p></p>")
|
||||||
|
message_stripped = models.TextField(blank=True, null=True)
|
||||||
|
sender = models.CharField(max_length=255)
|
||||||
|
triggered_by = models.ForeignKey("db.User", related_name="triggered_notifications", on_delete=models.SET_NULL, null=True)
|
||||||
|
receiver = models.ForeignKey("db.User", related_name="received_notifications", on_delete=models.CASCADE)
|
||||||
|
read_at = models.DateTimeField(null=True)
|
||||||
|
snoozed_till = models.DateTimeField(null=True)
|
||||||
|
archived_at = models.DateTimeField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Notification"
|
||||||
|
verbose_name_plural = "Notifications"
|
||||||
|
db_table = "notifications"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return name of the notifications"""
|
||||||
|
return f"{self.receiver.email} <{self.workspace.name}>"
|
@ -4,6 +4,7 @@ from django.conf import settings
|
|||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
|
|
||||||
# Modeule imports
|
# Modeule imports
|
||||||
from plane.db.mixins import AuditModel
|
from plane.db.mixins import AuditModel
|
||||||
@ -74,6 +75,15 @@ class Project(BaseModel):
|
|||||||
estimate = models.ForeignKey(
|
estimate = models.ForeignKey(
|
||||||
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
|
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
|
||||||
)
|
)
|
||||||
|
archive_in = models.IntegerField(
|
||||||
|
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||||
|
)
|
||||||
|
close_in = models.IntegerField(
|
||||||
|
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||||
|
)
|
||||||
|
default_state = models.ForeignKey(
|
||||||
|
"db.State", on_delete=models.SET_NULL, null=True, related_name="default_state"
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return name of the project"""
|
"""Return name of the project"""
|
||||||
|
@ -18,6 +18,13 @@ from sentry_sdk import capture_exception
|
|||||||
from slack_sdk import WebClient
|
from slack_sdk import WebClient
|
||||||
from slack_sdk.errors import SlackApiError
|
from slack_sdk.errors import SlackApiError
|
||||||
|
|
||||||
|
def get_default_onboarding():
|
||||||
|
return {
|
||||||
|
"profile_complete": False,
|
||||||
|
"workspace_create": False,
|
||||||
|
"workspace_invite": False,
|
||||||
|
"workspace_join": False,
|
||||||
|
}
|
||||||
|
|
||||||
class User(AbstractBaseUser, PermissionsMixin):
|
class User(AbstractBaseUser, PermissionsMixin):
|
||||||
id = models.UUIDField(
|
id = models.UUIDField(
|
||||||
@ -73,6 +80,8 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||||||
role = models.CharField(max_length=300, null=True, blank=True)
|
role = models.CharField(max_length=300, null=True, blank=True)
|
||||||
is_bot = models.BooleanField(default=False)
|
is_bot = models.BooleanField(default=False)
|
||||||
theme = models.JSONField(default=dict)
|
theme = models.JSONField(default=dict)
|
||||||
|
is_tour_completed = models.BooleanField(default=False)
|
||||||
|
onboarding_step = models.JSONField(default=get_default_onboarding)
|
||||||
|
|
||||||
USERNAME_FIELD = "email"
|
USERNAME_FIELD = "email"
|
||||||
|
|
||||||
|
@ -15,15 +15,15 @@ ROLE_CHOICES = (
|
|||||||
|
|
||||||
|
|
||||||
class Workspace(BaseModel):
|
class Workspace(BaseModel):
|
||||||
name = models.CharField(max_length=255, verbose_name="Workspace Name")
|
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
||||||
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="owner_workspace",
|
related_name="owner_workspace",
|
||||||
)
|
)
|
||||||
slug = models.SlugField(max_length=100, db_index=True, unique=True)
|
slug = models.SlugField(max_length=48, db_index=True, unique=True)
|
||||||
company_size = models.PositiveIntegerField(default=10)
|
organization_size = models.CharField(max_length=20)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return name of the Workspace"""
|
"""Return name of the Workspace"""
|
||||||
|
@ -35,6 +35,7 @@ INSTALLED_APPS = [
|
|||||||
"rest_framework_simplejwt.token_blacklist",
|
"rest_framework_simplejwt.token_blacklist",
|
||||||
"corsheaders",
|
"corsheaders",
|
||||||
"taggit",
|
"taggit",
|
||||||
|
"django_celery_beat",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -213,3 +214,4 @@ SIMPLE_JWT = {
|
|||||||
CELERY_TIMEZONE = TIME_ZONE
|
CELERY_TIMEZONE = TIME_ZONE
|
||||||
CELERY_TASK_SERIALIZER = 'json'
|
CELERY_TASK_SERIALIZER = 'json'
|
||||||
CELERY_ACCEPT_CONTENT = ['application/json']
|
CELERY_ACCEPT_CONTENT = ['application/json']
|
||||||
|
CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task",)
|
||||||
|
@ -10,16 +10,14 @@ from sentry_sdk.integrations.redis import RedisIntegration
|
|||||||
|
|
||||||
from .common import * # noqa
|
from .common import * # noqa
|
||||||
|
|
||||||
DEBUG = int(os.environ.get(
|
DEBUG = int(os.environ.get("DEBUG", 1)) == 1
|
||||||
"DEBUG", 1
|
|
||||||
)) == 1
|
|
||||||
|
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
"NAME": os.environ.get("PGUSER", "plane"),
|
"NAME": os.environ.get("PGUSER", "plane"),
|
||||||
"USER": "",
|
"USER": "",
|
||||||
"PASSWORD": "",
|
"PASSWORD": "",
|
||||||
@ -27,13 +25,11 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DOCKERIZED = int(os.environ.get(
|
DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1
|
||||||
"DOCKERIZED", 0
|
|
||||||
)) == 1
|
|
||||||
|
|
||||||
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
||||||
|
|
||||||
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||||
|
|
||||||
if DOCKERIZED:
|
if DOCKERIZED:
|
||||||
DATABASES["default"] = dj_database_url.config()
|
DATABASES["default"] = dj_database_url.config()
|
||||||
@ -63,7 +59,29 @@ if os.environ.get("SENTRY_DSN", False):
|
|||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
environment="local",
|
environment="local",
|
||||||
traces_sample_rate=0.7,
|
traces_sample_rate=0.7,
|
||||||
|
profiles_sample_rate=1.0,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "DEBUG",
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"*": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "DEBUG",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
REDIS_HOST = "localhost"
|
REDIS_HOST = "localhost"
|
||||||
REDIS_PORT = 6379
|
REDIS_PORT = 6379
|
||||||
@ -82,8 +100,9 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
|
|||||||
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
|
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
|
||||||
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
|
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
|
||||||
|
|
||||||
|
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
||||||
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
|
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
|
||||||
|
|
||||||
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
|
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
|
||||||
|
|
||||||
|
@ -13,13 +13,11 @@ from sentry_sdk.integrations.redis import RedisIntegration
|
|||||||
from .common import * # noqa
|
from .common import * # noqa
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DEBUG = int(os.environ.get(
|
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
|
||||||
"DEBUG", 0
|
|
||||||
)) == 1
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
"NAME": "plane",
|
"NAME": "plane",
|
||||||
"USER": os.environ.get("PGUSER", ""),
|
"USER": os.environ.get("PGUSER", ""),
|
||||||
"PASSWORD": os.environ.get("PGPASSWORD", ""),
|
"PASSWORD": os.environ.get("PGPASSWORD", ""),
|
||||||
@ -72,8 +70,12 @@ CORS_ALLOW_HEADERS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
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)):
|
if bool(os.environ.get("SENTRY_DSN", False)):
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
@ -84,11 +86,12 @@ if bool(os.environ.get("SENTRY_DSN", False)):
|
|||||||
traces_sample_rate=1,
|
traces_sample_rate=1,
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
environment="production",
|
environment="production",
|
||||||
|
profiles_sample_rate=1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
if DOCKERIZED and USE_MINIO:
|
if DOCKERIZED and USE_MINIO:
|
||||||
INSTALLED_APPS += ("storages",)
|
INSTALLED_APPS += ("storages",)
|
||||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
|
||||||
# The AWS access key to use.
|
# The AWS access key to use.
|
||||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
||||||
# The AWS secret access key to use.
|
# The AWS secret access key to use.
|
||||||
@ -96,7 +99,9 @@ if DOCKERIZED and USE_MINIO:
|
|||||||
# The name of the bucket to store files in.
|
# The name of the bucket to store files in.
|
||||||
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
||||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "http://plane-minio:9000")
|
AWS_S3_ENDPOINT_URL = os.environ.get(
|
||||||
|
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
|
||||||
|
)
|
||||||
# Default permissions
|
# Default permissions
|
||||||
AWS_DEFAULT_ACL = "public-read"
|
AWS_DEFAULT_ACL = "public-read"
|
||||||
AWS_QUERYSTRING_AUTH = False
|
AWS_QUERYSTRING_AUTH = False
|
||||||
@ -187,7 +192,10 @@ else:
|
|||||||
# extra characters appended.
|
# extra characters appended.
|
||||||
AWS_S3_FILE_OVERWRITE = False
|
AWS_S3_FILE_OVERWRITE = False
|
||||||
|
|
||||||
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
|
STORAGES["default"] = {
|
||||||
|
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||||
|
}
|
||||||
|
|
||||||
# AWS Settings End
|
# AWS Settings End
|
||||||
|
|
||||||
# Enable Connection Pooling (if desired)
|
# Enable Connection Pooling (if desired)
|
||||||
@ -202,9 +210,6 @@ ALLOWED_HOSTS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Simplified static file serving.
|
|
||||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
|
||||||
|
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|
||||||
@ -241,8 +246,9 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
|
|||||||
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
|
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
|
||||||
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
|
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
|
||||||
|
|
||||||
|
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
||||||
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
|
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
|
||||||
|
|
||||||
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
|
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
|
||||||
|
|
||||||
|
@ -11,13 +11,12 @@ from sentry_sdk.integrations.django import DjangoIntegration
|
|||||||
from sentry_sdk.integrations.redis import RedisIntegration
|
from sentry_sdk.integrations.redis import RedisIntegration
|
||||||
|
|
||||||
from .common import * # noqa
|
from .common import * # noqa
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DEBUG = int(os.environ.get(
|
DEBUG = int(os.environ.get("DEBUG", 1)) == 1
|
||||||
"DEBUG", 1
|
|
||||||
)) == 1
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
"NAME": os.environ.get("PGUSER", "plane"),
|
"NAME": os.environ.get("PGUSER", "plane"),
|
||||||
"USER": "",
|
"USER": "",
|
||||||
"PASSWORD": "",
|
"PASSWORD": "",
|
||||||
@ -48,13 +47,15 @@ ALLOWED_HOSTS = ["*"]
|
|||||||
# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD.
|
# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD.
|
||||||
CORS_ALLOW_ALL_ORIGINS = True
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
|
|
||||||
# Simplified static file serving.
|
STORAGES = {
|
||||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
"staticfiles": {
|
||||||
|
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Make true if running in a docker environment
|
# Make true if running in a docker environment
|
||||||
DOCKERIZED = int(os.environ.get(
|
DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1
|
||||||
"DOCKERIZED", 0
|
|
||||||
)) == 1
|
|
||||||
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||||
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
||||||
|
|
||||||
@ -66,6 +67,7 @@ sentry_sdk.init(
|
|||||||
traces_sample_rate=1,
|
traces_sample_rate=1,
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
environment="staging",
|
environment="staging",
|
||||||
|
profiles_sample_rate=1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# The AWS region to connect to.
|
# The AWS region to connect to.
|
||||||
@ -150,7 +152,9 @@ AWS_S3_SIGNATURE_VERSION = None
|
|||||||
AWS_S3_FILE_OVERWRITE = False
|
AWS_S3_FILE_OVERWRITE = False
|
||||||
|
|
||||||
# AWS Settings End
|
# AWS Settings End
|
||||||
|
STORAGES["default"] = {
|
||||||
|
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||||
|
}
|
||||||
|
|
||||||
# Enable Connection Pooling (if desired)
|
# Enable Connection Pooling (if desired)
|
||||||
# DATABASES['default']['ENGINE'] = 'django_postgrespool'
|
# DATABASES['default']['ENGINE'] = 'django_postgrespool'
|
||||||
@ -163,11 +167,6 @@ ALLOWED_HOSTS = [
|
|||||||
"*",
|
"*",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
|
|
||||||
# Simplified static file serving.
|
|
||||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
|
||||||
|
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|
||||||
@ -199,15 +198,19 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
|
|||||||
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
|
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
|
||||||
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
|
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
|
||||||
|
|
||||||
|
|
||||||
|
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
||||||
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
|
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
|
||||||
|
|
||||||
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
|
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
|
||||||
|
|
||||||
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
|
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
|
||||||
|
|
||||||
redis_url = os.environ.get("REDIS_URL")
|
redis_url = os.environ.get("REDIS_URL")
|
||||||
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
broker_url = (
|
||||||
|
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||||
|
)
|
||||||
|
|
||||||
CELERY_RESULT_BACKEND = broker_url
|
CELERY_RESULT_BACKEND = broker_url
|
||||||
CELERY_BROKER_URL = broker_url
|
CELERY_BROKER_URL = broker_url
|
||||||
|
@ -3,11 +3,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# from django.contrib import admin
|
# from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, include, re_path
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url, static
|
|
||||||
|
|
||||||
# from django.conf.urls.static import static
|
# from django.conf.urls.static import static
|
||||||
|
|
||||||
@ -18,11 +17,10 @@ urlpatterns = [
|
|||||||
path("", include("plane.web.urls")),
|
path("", include("plane.web.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = urlpatterns + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^__debug__/", include(debug_toolbar.urls)),
|
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||||
] + urlpatterns
|
] + urlpatterns
|
||||||
|
@ -166,16 +166,16 @@ def filter_target_date(params, filter, method):
|
|||||||
for query in target_dates:
|
for query in target_dates:
|
||||||
target_date_query = query.split(";")
|
target_date_query = query.split(";")
|
||||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||||
filter["target_date__gte"] = target_date_query[0]
|
filter["target_date__gt"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
filter["target_date__lte"] = target_date_query[0]
|
filter["target_date__lt"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
if params.get("target_date", None) and len(params.get("target_date")):
|
if params.get("target_date", None) and len(params.get("target_date")):
|
||||||
for query in params.get("target_date"):
|
for query in params.get("target_date"):
|
||||||
if query.get("timeline", "after") == "after":
|
if query.get("timeline", "after") == "after":
|
||||||
filter["target_date__gte"] = query.get("datetime")
|
filter["target_date__gt"] = query.get("datetime")
|
||||||
else:
|
else:
|
||||||
filter["target_date__lte"] = query.get("datetime")
|
filter["target_date__lt"] = query.get("datetime")
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
@ -1,31 +1,34 @@
|
|||||||
# base requirements
|
# base requirements
|
||||||
|
|
||||||
Django==3.2.19
|
Django==4.2.3
|
||||||
django-braces==1.15.0
|
django-braces==1.15.0
|
||||||
django-taggit==3.1.0
|
django-taggit==4.0.0
|
||||||
psycopg2==2.9.5
|
psycopg==3.1.9
|
||||||
django-oauth-toolkit==2.2.0
|
django-oauth-toolkit==2.3.0
|
||||||
mistune==2.0.4
|
mistune==3.0.1
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
redis==4.5.4
|
redis==4.6.0
|
||||||
django-nested-admin==4.0.2
|
django-nested-admin==4.0.2
|
||||||
django-cors-headers==3.13.0
|
django-cors-headers==4.1.0
|
||||||
whitenoise==6.3.0
|
whitenoise==6.5.0
|
||||||
django-allauth==0.52.0
|
django-allauth==0.54.0
|
||||||
faker==13.4.0
|
faker==18.11.2
|
||||||
django-filter==22.1
|
django-filter==23.2
|
||||||
jsonmodels==2.6.0
|
jsonmodels==2.6.0
|
||||||
djangorestframework-simplejwt==5.2.2
|
djangorestframework-simplejwt==5.2.2
|
||||||
sentry-sdk==1.14.0
|
sentry-sdk==1.27.0
|
||||||
django-s3-storage==0.13.11
|
django-s3-storage==0.14.0
|
||||||
django-crum==0.7.9
|
django-crum==0.7.9
|
||||||
django-guardian==2.4.0
|
django-guardian==2.4.0
|
||||||
dj_rest_auth==2.2.5
|
dj_rest_auth==2.2.5
|
||||||
google-auth==2.16.0
|
google-auth==2.21.0
|
||||||
google-api-python-client==2.75.0
|
google-api-python-client==2.92.0
|
||||||
django-redis==5.2.0
|
django-redis==5.3.0
|
||||||
uvicorn==0.20.0
|
uvicorn==0.22.0
|
||||||
channels==4.0.0
|
channels==4.0.0
|
||||||
openai==0.27.2
|
openai==0.27.8
|
||||||
slack-sdk==3.20.2
|
slack-sdk==3.21.3
|
||||||
celery==5.2.7
|
celery==5.3.1
|
||||||
|
django_celery_beat==2.5.0
|
||||||
|
psycopg-binary==3.1.9
|
||||||
|
psycopg-c==3.1.9
|
@ -1,3 +1,3 @@
|
|||||||
-r base.txt
|
-r base.txt
|
||||||
|
|
||||||
django-debug-toolbar==3.8.1
|
django-debug-toolbar==4.1.0
|
@ -1,12 +1,11 @@
|
|||||||
-r base.txt
|
-r base.txt
|
||||||
|
|
||||||
dj-database-url==1.2.0
|
dj-database-url==2.0.0
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
whitenoise==6.3.0
|
whitenoise==6.5.0
|
||||||
django-storages==1.13.2
|
django-storages==1.13.2
|
||||||
boto3==1.26.136
|
boto3==1.27.0
|
||||||
django-anymail==9.0
|
django-anymail==10.0
|
||||||
twilio==7.16.2
|
django-debug-toolbar==4.1.0
|
||||||
django-debug-toolbar==3.8.1
|
gevent==23.7.0
|
||||||
gevent==22.10.2
|
|
||||||
psycogreen==1.0.2
|
psycogreen==1.0.2
|
@ -20,7 +20,7 @@ ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
|||||||
COPY .gitignore .gitignore
|
COPY .gitignore .gitignore
|
||||||
COPY --from=builder /app/out/json/ .
|
COPY --from=builder /app/out/json/ .
|
||||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||||
RUN yarn install
|
RUN yarn install --network-timeout 500000
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
|
@ -32,6 +32,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
setError,
|
setError,
|
||||||
setValue,
|
setValue,
|
||||||
getValues,
|
getValues,
|
||||||
|
watch,
|
||||||
formState: { errors, isSubmitting, isValid, isDirty },
|
formState: { errors, isSubmitting, isValid, isDirty },
|
||||||
} = useForm<EmailCodeFormValues>({
|
} = useForm<EmailCodeFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -112,43 +113,35 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5 py-5 px-5">
|
{(codeSent || codeResent) && (
|
||||||
{(codeSent || codeResent) && (
|
<p className="text-center mt-4">
|
||||||
<div className="rounded-md bg-green-500/20 p-4">
|
We have sent the sign in code.
|
||||||
<div className="flex">
|
<br />
|
||||||
<div className="flex-shrink-0">
|
Please check your inbox at <span className="font-medium">{watch("email")}</span>
|
||||||
<CheckCircleIcon className="h-5 w-5 text-green-500" aria-hidden="true" />
|
</p>
|
||||||
</div>
|
)}
|
||||||
<div className="ml-3">
|
<form className="space-y-4 mt-10 sm:w-[360px] mx-auto">
|
||||||
<p className="text-sm font-medium text-green-500">
|
<div className="space-y-1">
|
||||||
{codeResent
|
|
||||||
? "Please check your mail for new code."
|
|
||||||
: "Please check your mail for code."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
register={register}
|
register={register}
|
||||||
validations={{
|
validations={{
|
||||||
required: "Email ID is required",
|
required: "Email address is required",
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||||
value
|
value
|
||||||
) || "Email ID is not valid",
|
) || "Email address is not valid",
|
||||||
}}
|
}}
|
||||||
error={errors.email}
|
error={errors.email}
|
||||||
placeholder="Enter your Email ID"
|
placeholder="Enter your email address..."
|
||||||
|
className="border-custom-border-300 h-[46px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{codeSent && (
|
{codeSent && (
|
||||||
<div>
|
<>
|
||||||
<Input
|
<Input
|
||||||
id="token"
|
id="token"
|
||||||
type="token"
|
type="token"
|
||||||
@ -158,14 +151,15 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
required: "Code is required",
|
required: "Code is required",
|
||||||
}}
|
}}
|
||||||
error={errors.token}
|
error={errors.token}
|
||||||
placeholder="Enter code"
|
placeholder="Enter code..."
|
||||||
|
className="border-custom-border-300 h-[46px]"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`mt-5 flex w-full justify-end text-xs outline-none ${
|
className={`flex w-full justify-end text-xs outline-none ${
|
||||||
isResendDisabled
|
isResendDisabled
|
||||||
? "cursor-default text-brand-secondary"
|
? "cursor-default text-custom-text-200"
|
||||||
: "cursor-pointer text-brand-accent"
|
: "cursor-pointer text-custom-primary-100"
|
||||||
} `}
|
} `}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCodeResending(true);
|
setIsCodeResending(true);
|
||||||
@ -178,46 +172,43 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
disabled={isResendDisabled}
|
disabled={isResendDisabled}
|
||||||
>
|
>
|
||||||
{resendCodeTimer > 0 ? (
|
{resendCodeTimer > 0 ? (
|
||||||
<p className="text-right">
|
<span className="text-right">Request new code in {resendCodeTimer} seconds</span>
|
||||||
Didn{"'"}t receive code? Get new code in {resendCodeTimer} seconds.
|
|
||||||
</p>
|
|
||||||
) : isCodeResending ? (
|
) : isCodeResending ? (
|
||||||
"Sending code..."
|
"Sending new code..."
|
||||||
) : errorResendingCode ? (
|
) : errorResendingCode ? (
|
||||||
"Please try again later"
|
"Please try again later"
|
||||||
) : (
|
) : (
|
||||||
"Resend code"
|
<span className="font-medium">Resend code</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
{codeSent ? (
|
||||||
|
<PrimaryButton
|
||||||
|
type="submit"
|
||||||
|
className="w-full text-center h-[46px]"
|
||||||
|
size="md"
|
||||||
|
onClick={handleSubmit(handleSignin)}
|
||||||
|
disabled={!isValid && isDirty}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Signing in..." : "Sign in"}
|
||||||
|
</PrimaryButton>
|
||||||
|
) : (
|
||||||
|
<PrimaryButton
|
||||||
|
className="w-full text-center h-[46px]"
|
||||||
|
size="md"
|
||||||
|
onClick={() => {
|
||||||
|
handleSubmit(onSubmit)().then(() => {
|
||||||
|
setResendCodeTimer(30);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isValid && isDirty}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Sending code..." : "Send sign in code"}
|
||||||
|
</PrimaryButton>
|
||||||
)}
|
)}
|
||||||
<div>
|
|
||||||
{codeSent ? (
|
|
||||||
<PrimaryButton
|
|
||||||
type="submit"
|
|
||||||
className="w-full text-center"
|
|
||||||
size="md"
|
|
||||||
onClick={handleSubmit(handleSignin)}
|
|
||||||
disabled={!isValid && isDirty}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? "Signing in..." : "Sign in"}
|
|
||||||
</PrimaryButton>
|
|
||||||
) : (
|
|
||||||
<PrimaryButton
|
|
||||||
className="w-full text-center"
|
|
||||||
size="md"
|
|
||||||
onClick={() => {
|
|
||||||
handleSubmit(onSubmit)().then(() => {
|
|
||||||
setResendCodeTimer(30);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
loading={isSubmitting || (!isValid && isDirty)}
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Sending code..." : "Send code"}
|
|
||||||
</PrimaryButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ import { useForm } from "react-hook-form";
|
|||||||
// components
|
// components
|
||||||
import { EmailResetPasswordForm } from "components/account";
|
import { EmailResetPasswordForm } from "components/account";
|
||||||
// ui
|
// ui
|
||||||
import { Input, SecondaryButton } from "components/ui";
|
import { Input, PrimaryButton } from "components/ui";
|
||||||
// types
|
// types
|
||||||
type EmailPasswordFormValues = {
|
type EmailPasswordFormValues = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -42,28 +42,39 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
|
||||||
|
{isResettingPassword
|
||||||
|
? "Reset your password"
|
||||||
|
: isSignUpPage
|
||||||
|
? "Sign up on Plane"
|
||||||
|
: "Sign in to Plane"}
|
||||||
|
</h1>
|
||||||
{isResettingPassword ? (
|
{isResettingPassword ? (
|
||||||
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
|
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
|
||||||
) : (
|
) : (
|
||||||
<form className="mt-5 py-5 px-5" onSubmit={handleSubmit(onSubmit)}>
|
<form
|
||||||
<div>
|
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
register={register}
|
register={register}
|
||||||
validations={{
|
validations={{
|
||||||
required: "Email ID is required",
|
required: "Email address is required",
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||||
value
|
value
|
||||||
) || "Email ID is not valid",
|
) || "Email address is not valid",
|
||||||
}}
|
}}
|
||||||
error={errors.email}
|
error={errors.email}
|
||||||
placeholder="Enter your email ID"
|
placeholder="Enter your email address..."
|
||||||
|
className="border-custom-border-300 h-[46px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5">
|
<div className="space-y-1">
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
@ -73,46 +84,45 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
|
|||||||
required: "Password is required",
|
required: "Password is required",
|
||||||
}}
|
}}
|
||||||
error={errors.password}
|
error={errors.password}
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password..."
|
||||||
|
className="border-custom-border-300 h-[46px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center justify-between">
|
<div className="text-right text-xs">
|
||||||
<div className="ml-auto text-sm">
|
{isSignUpPage ? (
|
||||||
{isSignUpPage ? (
|
<Link href="/">
|
||||||
<Link href="/">
|
<a className="text-custom-text-200 hover:text-custom-primary-100">
|
||||||
<a className="font-medium text-brand-accent hover:text-brand-accent">
|
Already have an account? Sign in.
|
||||||
Already have an account? Sign in.
|
</a>
|
||||||
</a>
|
</Link>
|
||||||
</Link>
|
) : (
|
||||||
) : (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => setIsResettingPassword(true)}
|
||||||
onClick={() => setIsResettingPassword(true)}
|
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||||
className="font-medium text-brand-accent hover:text-brand-accent"
|
>
|
||||||
>
|
Forgot your password?
|
||||||
Forgot your password?
|
</button>
|
||||||
</button>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5">
|
<div>
|
||||||
<SecondaryButton
|
<PrimaryButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full text-center"
|
className="w-full text-center h-[46px]"
|
||||||
disabled={!isValid && isDirty}
|
disabled={!isValid && isDirty}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSignUpPage
|
{isSignUpPage
|
||||||
? isSubmitting
|
? isSubmitting
|
||||||
? "Signing up..."
|
? "Signing up..."
|
||||||
: "Sign Up"
|
: "Sign up"
|
||||||
: isSubmitting
|
: isSubmitting
|
||||||
? "Signing in..."
|
? "Signing in..."
|
||||||
: "Sign In"}
|
: "Sign in"}
|
||||||
</SecondaryButton>
|
</PrimaryButton>
|
||||||
{!isSignUpPage && (
|
{!isSignUpPage && (
|
||||||
<Link href="/sign-up">
|
<Link href="/sign-up">
|
||||||
<a className="block font-medium text-brand-accent hover:text-brand-accent text-sm mt-1">
|
<a className="block text-custom-text-200 hover:text-custom-primary-100 text-xs mt-4">
|
||||||
Don{"'"}t have an account? Sign up.
|
Don{"'"}t have an account? Sign up.
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -59,32 +59,36 @@ export const EmailResetPasswordForm: React.FC<Props> = ({ setIsResettingPassword
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="mt-5 py-5 px-5" onSubmit={handleSubmit(forgotPassword)}>
|
<form
|
||||||
<div>
|
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
||||||
|
onSubmit={handleSubmit(forgotPassword)}
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
register={register}
|
register={register}
|
||||||
validations={{
|
validations={{
|
||||||
required: "Email ID is required",
|
required: "Email address is required",
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||||
value
|
value
|
||||||
) || "Email ID is not valid",
|
) || "Email address is not valid",
|
||||||
}}
|
}}
|
||||||
error={errors.email}
|
error={errors.email}
|
||||||
placeholder="Enter registered Email ID"
|
placeholder="Enter registered email address.."
|
||||||
|
className="border-custom-border-300 h-[46px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex items-center gap-2">
|
<div className="mt-5 flex flex-col-reverse sm:flex-row items-center gap-2">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
className="w-full text-center"
|
className="w-full text-center h-[46px]"
|
||||||
onClick={() => setIsResettingPassword(false)}
|
onClick={() => setIsResettingPassword(false)}
|
||||||
>
|
>
|
||||||
Go Back
|
Go Back
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<PrimaryButton type="submit" className="w-full text-center" loading={isSubmitting}>
|
<PrimaryButton type="submit" className="w-full text-center h-[46px]" loading={isSubmitting}>
|
||||||
{isSubmitting ? "Sending link..." : "Send reset link"}
|
{isSubmitting ? "Sending link..." : "Send reset link"}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { useEffect, useState, FC } from "react";
|
import { useEffect, useState, FC } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// next-themes
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
// images
|
// images
|
||||||
import githubImage from "/public/logos/github-black.png";
|
import githubBlackImage from "/public/logos/github-black.png";
|
||||||
|
import githubWhiteImage from "/public/logos/github-white.png";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_GITHUB_ID } = process.env;
|
const { NEXT_PUBLIC_GITHUB_ID } = process.env;
|
||||||
|
|
||||||
@ -11,15 +16,15 @@ export interface GithubLoginButtonProps {
|
|||||||
handleSignIn: React.Dispatch<string>;
|
handleSignIn: React.Dispatch<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn }) => {
|
||||||
const { handleSignIn } = props;
|
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
||||||
// router
|
const [gitCode, setGitCode] = useState<null | string>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
query: { code },
|
query: { code },
|
||||||
} = useRouter();
|
} = useRouter();
|
||||||
// states
|
|
||||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
const { theme } = useTheme();
|
||||||
const [gitCode, setGitCode] = useState<null | string>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (code && !gitCode) {
|
if (code && !gitCode) {
|
||||||
@ -35,13 +40,18 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex justify-center items-center px-[3px]">
|
<div className="w-full flex justify-center items-center">
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||||
>
|
>
|
||||||
<button className="flex w-full items-center justify-center gap-3 rounded border border-brand-base p-2 text-sm font-medium text-brand-secondary duration-300 hover:bg-brand-surface-2">
|
<button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]">
|
||||||
<Image src={githubImage} height={20} width={20} color="#000" alt="GitHub Logo" />
|
<Image
|
||||||
<span>Sign In with Github</span>
|
src={theme === "dark" ? githubWhiteImage : githubBlackImage}
|
||||||
|
height={20}
|
||||||
|
width={20}
|
||||||
|
alt="GitHub Logo"
|
||||||
|
/>
|
||||||
|
<span>Sign in with GitHub</span>
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react";
|
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react";
|
||||||
// next
|
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
export interface IGoogleLoginButton {
|
export interface IGoogleLoginButton {
|
||||||
@ -8,18 +8,18 @@ export interface IGoogleLoginButton {
|
|||||||
styles?: CSSProperties;
|
styles?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => {
|
||||||
const { handleSignIn } = props;
|
|
||||||
|
|
||||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||||
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
|
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
|
||||||
|
|
||||||
const loadScript = useCallback(() => {
|
const loadScript = useCallback(() => {
|
||||||
if (!googleSignInButton.current || gsiScriptLoaded) return;
|
if (!googleSignInButton.current || gsiScriptLoaded) return;
|
||||||
|
|
||||||
window?.google?.accounts.id.initialize({
|
window?.google?.accounts.id.initialize({
|
||||||
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
|
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
|
||||||
callback: handleSignIn,
|
callback: handleSignIn,
|
||||||
});
|
});
|
||||||
|
|
||||||
window?.google?.accounts.id.renderButton(
|
window?.google?.accounts.id.renderButton(
|
||||||
googleSignInButton.current,
|
googleSignInButton.current,
|
||||||
{
|
{
|
||||||
@ -27,11 +27,13 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
|||||||
theme: "outline",
|
theme: "outline",
|
||||||
size: "large",
|
size: "large",
|
||||||
logo_alignment: "center",
|
logo_alignment: "center",
|
||||||
width: "410",
|
width: "360",
|
||||||
text: "continue_with",
|
text: "signin_with",
|
||||||
} as GsiButtonConfiguration // customization attributes
|
} as GsiButtonConfiguration // customization attributes
|
||||||
);
|
);
|
||||||
|
|
||||||
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
|
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
|
||||||
|
|
||||||
setGsiScriptLoaded(true);
|
setGsiScriptLoaded(true);
|
||||||
}, [handleSignIn, gsiScriptLoaded]);
|
}, [handleSignIn, gsiScriptLoaded]);
|
||||||
|
|
||||||
@ -48,7 +50,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
|||||||
<>
|
<>
|
||||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||||
<div
|
<div
|
||||||
className="overflow-hidden rounded w-full flex justify-center items-center"
|
className="overflow-hidden rounded w-full flex justify-center items-center !text-sm !font-medium !text-custom-text-100"
|
||||||
id="googleSignInButton"
|
id="googleSignInButton"
|
||||||
ref={googleSignInButton}
|
ref={googleSignInButton}
|
||||||
/>
|
/>
|
||||||
|
@ -97,7 +97,7 @@ export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClos
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
@ -111,10 +111,13 @@ export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClos
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div>
|
<div>
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||||
|
>
|
||||||
Save Analytics
|
Save Analytics
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
|
@ -61,7 +61,7 @@ export const CustomAnalytics: React.FC<Props> = ({
|
|||||||
<AnalyticsSelectBar
|
<AnalyticsSelectBar
|
||||||
control={control}
|
control={control}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
projects={projects}
|
projects={projects ?? []}
|
||||||
params={params}
|
params={params}
|
||||||
fullScreen={fullScreen}
|
fullScreen={fullScreen}
|
||||||
isProjectLevel={isProjectLevel}
|
isProjectLevel={isProjectLevel}
|
||||||
@ -86,7 +86,7 @@ export const CustomAnalytics: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full place-items-center p-5">
|
<div className="grid h-full place-items-center p-5">
|
||||||
<div className="space-y-4 text-brand-secondary">
|
<div className="space-y-4 text-custom-text-200">
|
||||||
<p className="text-sm">No matching issues found. Try changing the parameters.</p>
|
<p className="text-sm">No matching issues found. Try changing the parameters.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -104,7 +104,7 @@ export const CustomAnalytics: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full place-items-center p-5">
|
<div className="grid h-full place-items-center p-5">
|
||||||
<div className="space-y-4 text-brand-secondary">
|
<div className="space-y-4 text-custom-text-200">
|
||||||
<p className="text-sm">There was some error in fetching the data.</p>
|
<p className="text-sm">There was some error in fetching the data.</p>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
@ -31,7 +31,7 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
|
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||||
<span
|
<span
|
||||||
className="h-3 w-3 rounded"
|
className="h-3 w-3 rounded"
|
||||||
style={{
|
style={{
|
||||||
@ -39,7 +39,7 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`font-medium text-brand-secondary ${
|
className={`font-medium text-custom-text-200 ${
|
||||||
params.segment
|
params.segment
|
||||||
? params.segment === "priority" || params.segment === "state__group"
|
? params.segment === "priority" || params.segment === "state__group"
|
||||||
? "capitalize"
|
? "capitalize"
|
||||||
|
@ -111,7 +111,6 @@ export const AnalyticsGraph: React.FC<Props> = ({
|
|||||||
: undefined,
|
: undefined,
|
||||||
}}
|
}}
|
||||||
theme={{
|
theme={{
|
||||||
background: "rgb(var(--color-bg-base))",
|
|
||||||
axis: {},
|
axis: {},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -29,7 +29,7 @@ export const AnalyticsSelectBar: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{!isProjectLevel && (
|
{!isProjectLevel && (
|
||||||
<div>
|
<div>
|
||||||
<h6 className="text-xs text-brand-secondary">Project</h6>
|
<h6 className="text-xs text-custom-text-200">Project</h6>
|
||||||
<Controller
|
<Controller
|
||||||
name="project"
|
name="project"
|
||||||
control={control}
|
control={control}
|
||||||
@ -40,7 +40,7 @@ export const AnalyticsSelectBar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h6 className="text-xs text-brand-secondary">Measure (y-axis)</h6>
|
<h6 className="text-xs text-custom-text-200">Measure (y-axis)</h6>
|
||||||
<Controller
|
<Controller
|
||||||
name="y_axis"
|
name="y_axis"
|
||||||
control={control}
|
control={control}
|
||||||
@ -50,7 +50,7 @@ export const AnalyticsSelectBar: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 className="text-xs text-brand-secondary">Dimension (x-axis)</h6>
|
<h6 className="text-xs text-custom-text-200">Dimension (x-axis)</h6>
|
||||||
<Controller
|
<Controller
|
||||||
name="x_axis"
|
name="x_axis"
|
||||||
control={control}
|
control={control}
|
||||||
@ -67,7 +67,7 @@ export const AnalyticsSelectBar: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 className="text-xs text-brand-secondary">Group</h6>
|
<h6 className="text-xs text-custom-text-200">Group</h6>
|
||||||
<Controller
|
<Controller
|
||||||
name="segment"
|
name="segment"
|
||||||
control={control}
|
control={control}
|
||||||
|
@ -23,13 +23,14 @@ import {
|
|||||||
import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
|
import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDate } from "helpers/date-time.helper";
|
import { renderShortDate } from "helpers/date-time.helper";
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
import { truncateText } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
IAnalyticsParams,
|
IAnalyticsParams,
|
||||||
IAnalyticsResponse,
|
IAnalyticsResponse,
|
||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
IExportAnalyticsFormData,
|
IExportAnalyticsFormData,
|
||||||
IProject,
|
|
||||||
IWorkspace,
|
IWorkspace,
|
||||||
} from "types";
|
} from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -178,23 +179,23 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectedProjects =
|
const selectedProjects =
|
||||||
params.project && params.project.length > 0 ? params.project : projects.map((p) => p.id);
|
params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
|
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
|
||||||
fullScreen
|
fullScreen
|
||||||
? "border-l border-brand-base md:h-full md:border-l md:border-brand-base md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5"
|
? "border-l border-custom-border-200 md:h-full md:border-l md:border-custom-border-200 md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<div className="flex items-center gap-1 bg-brand-surface-2 rounded-md px-3 py-1 text-brand-secondary text-xs">
|
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
||||||
<LayerDiagonalIcon height={14} width={14} />
|
<LayerDiagonalIcon height={14} width={14} />
|
||||||
{analytics ? analytics.total : "..."} Issues
|
{analytics ? analytics.total : "..."} Issues
|
||||||
</div>
|
</div>
|
||||||
{isProjectLevel && (
|
{isProjectLevel && (
|
||||||
<div className="flex items-center gap-1 bg-brand-surface-2 rounded-md px-3 py-1 text-brand-secondary text-xs">
|
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
||||||
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
||||||
{renderShortDate(
|
{renderShortDate(
|
||||||
(cycleId
|
(cycleId
|
||||||
@ -206,7 +207,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full overflow-hidden">
|
<div className="h-full w-full overflow-hidden">
|
||||||
{fullScreen ? (
|
{fullScreen ? (
|
||||||
<>
|
<>
|
||||||
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
||||||
@ -214,61 +215,62 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
<h4 className="font-medium">Selected Projects</h4>
|
<h4 className="font-medium">Selected Projects</h4>
|
||||||
<div className="space-y-6 mt-4 h-full overflow-y-auto">
|
<div className="space-y-6 mt-4 h-full overflow-y-auto">
|
||||||
{selectedProjects.map((projectId) => {
|
{selectedProjects.map((projectId) => {
|
||||||
const project: IProject = projects.find((p) => p.id === projectId);
|
const project = projects?.find((p) => p.id === projectId);
|
||||||
|
|
||||||
return (
|
if (project)
|
||||||
<div key={project.id}>
|
return (
|
||||||
<div className="text-sm flex items-center gap-1">
|
<div key={project.id} className="w-full">
|
||||||
{project.emoji ? (
|
<div className="text-sm flex items-center gap-1">
|
||||||
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
{project.emoji ? (
|
||||||
{String.fromCodePoint(parseInt(project.emoji))}
|
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
||||||
</span>
|
{renderEmoji(project.emoji)}
|
||||||
) : project.icon_prop ? (
|
|
||||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
|
||||||
<span
|
|
||||||
style={{ color: project.icon_prop.color }}
|
|
||||||
className="material-symbols-rounded text-lg"
|
|
||||||
>
|
|
||||||
{project.icon_prop.name}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
) : project.icon_prop ? (
|
||||||
) : (
|
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
<span
|
||||||
{project?.name.charAt(0)}
|
style={{ color: project.icon_prop.color }}
|
||||||
</span>
|
className="material-symbols-rounded text-lg"
|
||||||
)}
|
>
|
||||||
<h5 className="break-words">
|
{project.icon_prop.name}
|
||||||
{project.name}
|
</span>
|
||||||
<span className="text-brand-secondary text-xs ml-1">
|
</div>
|
||||||
({project.identifier})
|
) : (
|
||||||
</span>
|
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||||
</h5>
|
{project?.name.charAt(0)}
|
||||||
</div>
|
</span>
|
||||||
<div className="mt-4 space-y-3 pl-2">
|
)}
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
<h5 className="flex items-center gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<p className="break-words">{truncateText(project.name, 20)}</p>
|
||||||
<UserGroupIcon className="h-4 w-4 text-brand-secondary" />
|
<span className="text-custom-text-200 text-xs ml-1">
|
||||||
<h6>Total members</h6>
|
({project.identifier})
|
||||||
</div>
|
</span>
|
||||||
<span className="text-brand-secondary">{project.total_members}</span>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
<div className="mt-4 space-y-3 pl-2 w-full">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between gap-2 text-xs">
|
||||||
<ContrastIcon height={16} width={16} />
|
<div className="flex items-center gap-2">
|
||||||
<h6>Total cycles</h6>
|
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
<h6>Total members</h6>
|
||||||
|
</div>
|
||||||
|
<span className="text-custom-text-200">{project.total_members}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-brand-secondary">{project.total_cycles}</span>
|
<div className="flex items-center justify-between gap-2 text-xs">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
<ContrastIcon height={16} width={16} />
|
||||||
<div className="flex items-center gap-2">
|
<h6>Total cycles</h6>
|
||||||
<UserGroupIcon className="h-4 w-4 text-brand-secondary" />
|
</div>
|
||||||
<h6>Total modules</h6>
|
<span className="text-custom-text-200">{project.total_cycles}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-2 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
<h6>Total modules</h6>
|
||||||
|
</div>
|
||||||
|
<span className="text-custom-text-200">{project.total_modules}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-brand-secondary">{project.total_modules}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -279,13 +281,13 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
<h4 className="font-medium break-words">Analytics for {cycleDetails.name}</h4>
|
<h4 className="font-medium break-words">Analytics for {cycleDetails.name}</h4>
|
||||||
<div className="space-y-4 mt-4">
|
<div className="space-y-4 mt-4">
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<h6 className="text-brand-secondary">Lead</h6>
|
<h6 className="text-custom-text-200">Lead</h6>
|
||||||
<span>
|
<span>
|
||||||
{cycleDetails.owned_by?.first_name} {cycleDetails.owned_by?.last_name}
|
{cycleDetails.owned_by?.first_name} {cycleDetails.owned_by?.last_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<h6 className="text-brand-secondary">Start Date</h6>
|
<h6 className="text-custom-text-200">Start Date</h6>
|
||||||
<span>
|
<span>
|
||||||
{cycleDetails.start_date && cycleDetails.start_date !== ""
|
{cycleDetails.start_date && cycleDetails.start_date !== ""
|
||||||
? renderShortDate(cycleDetails.start_date)
|
? renderShortDate(cycleDetails.start_date)
|
||||||
@ -293,7 +295,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<h6 className="text-brand-secondary">Target Date</h6>
|
<h6 className="text-custom-text-200">Target Date</h6>
|
||||||
<span>
|
<span>
|
||||||
{cycleDetails.end_date && cycleDetails.end_date !== ""
|
{cycleDetails.end_date && cycleDetails.end_date !== ""
|
||||||
? renderShortDate(cycleDetails.end_date)
|
? renderShortDate(cycleDetails.end_date)
|
||||||
@ -307,14 +309,14 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
<h4 className="font-medium break-words">Analytics for {moduleDetails.name}</h4>
|
<h4 className="font-medium break-words">Analytics for {moduleDetails.name}</h4>
|
||||||
<div className="space-y-4 mt-4">
|
<div className="space-y-4 mt-4">
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<h6 className="text-brand-secondary">Lead</h6>
|
<h6 className="text-custom-text-200">Lead</h6>
|
||||||
<span>
|
<span>
|
||||||
{moduleDetails.lead_detail?.first_name}{" "}
|
{moduleDetails.lead_detail?.first_name}{" "}
|
||||||
{moduleDetails.lead_detail?.last_name}
|
{moduleDetails.lead_detail?.last_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<h6 className="text-brand-secondary">Start Date</h6>
|
<h6 className="text-custom-text-200">Start Date</h6>
|
||||||
<span>
|
<span>
|
||||||
{moduleDetails.start_date && moduleDetails.start_date !== ""
|
{moduleDetails.start_date && moduleDetails.start_date !== ""
|
||||||
? renderShortDate(moduleDetails.start_date)
|
? renderShortDate(moduleDetails.start_date)
|
||||||
@ -322,7 +324,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<h6 className="text-brand-secondary">Target Date</h6>
|
<h6 className="text-custom-text-200">Target Date</h6>
|
||||||
<span>
|
<span>
|
||||||
{moduleDetails.target_date && moduleDetails.target_date !== ""
|
{moduleDetails.target_date && moduleDetails.target_date !== ""
|
||||||
? renderShortDate(moduleDetails.target_date)
|
? renderShortDate(moduleDetails.target_date)
|
||||||
@ -336,7 +338,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{projectDetails?.emoji ? (
|
{projectDetails?.emoji ? (
|
||||||
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
||||||
{String.fromCodePoint(parseInt(projectDetails.emoji))}
|
{renderEmoji(projectDetails.emoji)}
|
||||||
</div>
|
</div>
|
||||||
) : projectDetails?.icon_prop ? (
|
) : projectDetails?.icon_prop ? (
|
||||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||||
@ -356,7 +358,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-4 mt-4">
|
<div className="space-y-4 mt-4">
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<h6 className="text-brand-secondary">Network</h6>
|
<h6 className="text-custom-text-200">Network</h6>
|
||||||
<span>
|
<span>
|
||||||
{
|
{
|
||||||
NETWORK_CHOICES[
|
NETWORK_CHOICES[
|
||||||
|
@ -37,9 +37,9 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
|||||||
<div className="flow-root">
|
<div className="flow-root">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="inline-block min-w-full align-middle">
|
<div className="inline-block min-w-full align-middle">
|
||||||
<table className="min-w-full divide-y divide-brand-base whitespace-nowrap border-y border-brand-base">
|
<table className="min-w-full divide-y divide-custom-border-200 whitespace-nowrap border-y border-custom-border-200">
|
||||||
<thead className="bg-brand-surface-2">
|
<thead className="bg-custom-background-80">
|
||||||
<tr className="divide-x divide-brand-base text-sm text-brand-base">
|
<tr className="divide-x divide-custom-border-200 text-sm text-custom-text-100">
|
||||||
<th scope="col" className="py-3 px-2.5 text-left font-medium">
|
<th scope="col" className="py-3 px-2.5 text-left font-medium">
|
||||||
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
|
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
|
||||||
</th>
|
</th>
|
||||||
@ -80,11 +80,11 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
|||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-brand-base">
|
<tbody className="divide-y divide-custom-border-200">
|
||||||
{barGraphData.data.map((item, index) => (
|
{barGraphData.data.map((item, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={`table-row-${index}`}
|
key={`table-row-${index}`}
|
||||||
className="divide-x divide-brand-base text-xs text-brand-secondary"
|
className="divide-x divide-custom-border-200 text-xs text-custom-text-200"
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
|
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
|
||||||
|
@ -150,16 +150,16 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0 z-30 h-full bg-brand-surface-1 ${
|
className={`absolute top-0 z-30 h-full bg-custom-background-90 ${
|
||||||
fullScreen ? "p-2 w-full" : "w-1/2"
|
fullScreen ? "p-2 w-full" : "w-1/2"
|
||||||
} ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`}
|
} ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex h-full flex-col overflow-hidden border-brand-base bg-brand-base text-left ${
|
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
|
||||||
fullScreen ? "rounded-lg border" : "border-l"
|
fullScreen ? "rounded-lg border" : "border-l"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-4 bg-brand-base px-5 py-4 text-sm">
|
<div className="flex items-center justify-between gap-4 bg-custom-background-100 px-5 py-4 text-sm">
|
||||||
<h3 className="break-words">
|
<h3 className="break-words">
|
||||||
Analytics for{" "}
|
Analytics for{" "}
|
||||||
{cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
|
{cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
|
||||||
@ -167,7 +167,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center p-1 text-brand-secondary hover:text-brand-base"
|
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
||||||
onClick={() => setFullScreen((prevData) => !prevData)}
|
onClick={() => setFullScreen((prevData) => !prevData)}
|
||||||
>
|
>
|
||||||
{fullScreen ? (
|
{fullScreen ? (
|
||||||
@ -178,7 +178,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center p-1 text-brand-secondary hover:text-brand-base"
|
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
<XMarkIcon className="h-4 w-4" />
|
<XMarkIcon className="h-4 w-4" />
|
||||||
@ -186,13 +186,13 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tab.Group as={Fragment}>
|
<Tab.Group as={Fragment}>
|
||||||
<Tab.List as="div" className="space-x-2 border-b border-brand-base p-5 pt-0">
|
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 p-5 pt-0">
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab}
|
key={tab}
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-surface-2 ${
|
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||||
selected ? "bg-brand-surface-2" : ""
|
selected ? "bg-custom-background-80" : ""
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
onClick={() => trackAnalyticsEvent(tab)}
|
onClick={() => trackAnalyticsEvent(tab)}
|
||||||
|
@ -10,10 +10,10 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||||
<div className="space-y-3 rounded-[10px] border border-brand-base p-3">
|
<div className="space-y-3 rounded-[10px] border border-custom-border-200 p-3">
|
||||||
<h5 className="text-xs text-red-500">DEMAND</h5>
|
<h5 className="text-xs text-red-500">DEMAND</h5>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-brand-bas text-base font-medium">Total open tasks</h4>
|
<h4 className="text-custom-text-100 text-base font-medium">Total open tasks</h4>
|
||||||
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
|
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -31,13 +31,13 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<h6 className="capitalize">{group.state_group}</h6>
|
<h6 className="capitalize">{group.state_group}</h6>
|
||||||
<span className="ml-1 rounded-3xl bg-brand-surface-2 px-2 py-0.5 text-[0.65rem] text-brand-secondary">
|
<span className="ml-1 rounded-3xl bg-custom-background-80 px-2 py-0.5 text-[0.65rem] text-custom-text-200">
|
||||||
{group.state_count}
|
{group.state_count}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-brand-secondary">{percentage}%</p>
|
<p className="text-custom-text-200">{percentage}%</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bar relative h-1 w-full rounded bg-brand-surface-2">
|
<div className="bar relative h-1 w-full rounded bg-custom-background-80">
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 left-0 h-1 rounded duration-300"
|
className="absolute top-0 left-0 h-1 rounded duration-300"
|
||||||
style={{
|
style={{
|
||||||
@ -50,8 +50,8 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
|
<div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||||
<p className="flex items-center gap-1 text-brand-secondary">
|
<p className="flex items-center gap-1 text-custom-text-200">
|
||||||
<PlayIcon className="h-4 w-4 -rotate-90" aria-hidden="true" />
|
<PlayIcon className="h-4 w-4 -rotate-90" aria-hidden="true" />
|
||||||
<span>Estimate Demand:</span>
|
<span>Estimate Demand:</span>
|
||||||
</p>
|
</p>
|
||||||
|
@ -10,7 +10,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
||||||
<div className="p-3 border border-brand-base rounded-[10px]">
|
<div className="p-3 border border-custom-border-200 rounded-[10px]">
|
||||||
<h6 className="text-base font-medium">{title}</h6>
|
<h6 className="text-base font-medium">{title}</h6>
|
||||||
{users.length > 0 ? (
|
{users.length > 0 ? (
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
@ -33,7 +33,7 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
|||||||
{user.firstName !== "" ? user.firstName[0] : "?"}
|
{user.firstName !== "" ? user.firstName[0] : "?"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="break-words text-brand-secondary">
|
<span className="break-words text-custom-text-200">
|
||||||
{user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"}
|
{user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -42,7 +42,7 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-brand-secondary text-center text-sm py-8">No matching data found.</div>
|
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -88,7 +88,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full place-items-center p-5">
|
<div className="grid h-full place-items-center p-5">
|
||||||
<div className="space-y-4 text-brand-secondary">
|
<div className="space-y-4 text-custom-text-200">
|
||||||
<p className="text-sm">There was some error in fetching the data.</p>
|
<p className="text-sm">There was some error in fetching the data.</p>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<PrimaryButton onClick={() => mutateDefaultAnalytics()}>Refresh</PrimaryButton>
|
<PrimaryButton onClick={() => mutateDefaultAnalytics()}>Refresh</PrimaryButton>
|
||||||
|
@ -8,9 +8,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||||
<div className="rounded-[10px] border border-brand-base">
|
<div className="rounded-[10px] border border-custom-border-200">
|
||||||
<h5 className="p-3 text-xs text-green-500">SCOPE</h5>
|
<h5 className="p-3 text-xs text-green-500">SCOPE</h5>
|
||||||
<div className="divide-y divide-brand-base">
|
<div className="divide-y divide-custom-border-200">
|
||||||
<div>
|
<div>
|
||||||
<h6 className="px-3 text-base font-medium">Pending issues</h6>
|
<h6 className="px-3 text-base font-medium">Pending issues</h6>
|
||||||
{defaultAnalytics.pending_issue_user.length > 0 ? (
|
{defaultAnalytics.pending_issue_user.length > 0 ? (
|
||||||
@ -27,8 +27,8 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
|
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||||
<span className="font-medium text-brand-secondary">
|
<span className="font-medium text-custom-text-200">
|
||||||
{assignee
|
{assignee
|
||||||
? assignee.assignees__first_name + " " + assignee.assignees__last_name
|
? assignee.assignees__first_name + " " + assignee.assignees__last_name
|
||||||
: "No assignee"}
|
: "No assignee"}
|
||||||
@ -69,12 +69,11 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
}}
|
}}
|
||||||
margin={{ top: 20 }}
|
margin={{ top: 20 }}
|
||||||
theme={{
|
theme={{
|
||||||
background: "rgb(var(--color-bg-base))",
|
|
||||||
axis: {},
|
axis: {},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-brand-secondary text-center text-sm py-8">
|
<div className="text-custom-text-200 text-center text-sm py-8">
|
||||||
No matching data found.
|
No matching data found.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -9,54 +9,46 @@ type Props = {
|
|||||||
defaultAnalytics: IDefaultAnalyticsResponse;
|
defaultAnalytics: IDefaultAnalyticsResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => {
|
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||||
const currentMonth = new Date().getMonth();
|
<div className="py-3 border border-custom-border-200 rounded-[10px]">
|
||||||
const startMonth = Math.floor(currentMonth / 3) * 3 + 1;
|
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
|
||||||
const quarterMonthsList = [startMonth, startMonth + 1, startMonth + 2];
|
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
|
||||||
|
<LineGraph
|
||||||
return (
|
data={[
|
||||||
<div className="py-3 border border-brand-base rounded-[10px]">
|
{
|
||||||
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
|
id: "issues_closed",
|
||||||
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
|
color: "rgb(var(--color-primary-100))",
|
||||||
<LineGraph
|
data: MONTHS_LIST.map((month) => ({
|
||||||
data={[
|
x: month.label.substring(0, 3),
|
||||||
{
|
y:
|
||||||
id: "issues_closed",
|
defaultAnalytics.issue_completed_month_wise.find(
|
||||||
color: "rgb(var(--color-accent))",
|
(data) => data.month === month.value
|
||||||
data: MONTHS_LIST.map((month) => ({
|
)?.count || 0,
|
||||||
x: month.label.substring(0, 3),
|
})),
|
||||||
y:
|
},
|
||||||
defaultAnalytics.issue_completed_month_wise.find(
|
]}
|
||||||
(data) => data.month === month.value
|
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map(
|
||||||
)?.count || 0,
|
(data) => data.count
|
||||||
})),
|
)}
|
||||||
},
|
height="300px"
|
||||||
]}
|
colors={(datum) => datum.color}
|
||||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => {
|
curve="monotoneX"
|
||||||
if (quarterMonthsList.includes(data.month)) return data.count;
|
margin={{ top: 20 }}
|
||||||
|
enableSlices="x"
|
||||||
return 0;
|
sliceTooltip={(datum) => (
|
||||||
})}
|
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||||
height="300px"
|
{datum.slice.points[0].data.yFormatted}
|
||||||
colors={(datum) => datum.color}
|
<span className="text-custom-text-200"> issues closed in </span>
|
||||||
curve="monotoneX"
|
{datum.slice.points[0].data.xFormatted}
|
||||||
margin={{ top: 20 }}
|
</div>
|
||||||
enableSlices="x"
|
)}
|
||||||
sliceTooltip={(datum) => (
|
theme={{
|
||||||
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
|
background: "rgb(var(--color-background-100))",
|
||||||
{datum.slice.points[0].data.yFormatted}
|
}}
|
||||||
<span className="text-brand-secondary"> issues closed in </span>
|
enableArea
|
||||||
{datum.slice.points[0].data.xFormatted}
|
/>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
|
||||||
theme={{
|
)}
|
||||||
background: "rgb(var(--color-bg-base))",
|
</div>
|
||||||
}}
|
);
|
||||||
enableArea
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="text-brand-secondary text-center text-sm py-8">No matching data found.</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -15,7 +15,7 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
|
|||||||
query: project.name + project.identifier,
|
query: project.name + project.identifier,
|
||||||
content: (
|
content: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-brand-secondary text-[0.65rem]">{project.identifier}</span>
|
<span className="text-custom-text-200 text-[0.65rem]">{project.identifier}</span>
|
||||||
{project.name}
|
{project.name}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
@ -23,7 +23,7 @@ export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => {
|
|||||||
label={
|
label={
|
||||||
<span>
|
<span>
|
||||||
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label ?? (
|
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label ?? (
|
||||||
<span className="text-brand-secondary">No value</span>
|
<span className="text-custom-text-200">No value</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-brand-surface-1 text-center">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-100 text-center">
|
||||||
<div className="h-44 w-72">
|
<div className="h-44 w-72">
|
||||||
<Image
|
<Image
|
||||||
src={type === "project" ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg}
|
src={type === "project" ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg}
|
||||||
@ -31,16 +31,16 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
|||||||
alt="ProjectSettingImg"
|
alt="ProjectSettingImg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-medium text-brand-base">
|
<h1 className="text-xl font-medium text-custom-text-100">
|
||||||
Oops! You are not authorized to view this page
|
Oops! You are not authorized to view this page
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="w-full max-w-md text-base text-brand-secondary">
|
<div className="w-full max-w-md text-base text-custom-text-200">
|
||||||
{user ? (
|
{user ? (
|
||||||
<p>
|
<p>
|
||||||
You have signed in as {user.email}. <br />
|
You have signed in as {user.email}. <br />
|
||||||
<Link href={`/?next=${currentPath}`}>
|
<Link href={`/?next=${currentPath}`}>
|
||||||
<a className="font-medium text-brand-base">Sign in</a>
|
<a className="font-medium text-custom-text-100">Sign in</a>
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
with different account that has access to this page.
|
with different account that has access to this page.
|
||||||
</p>
|
</p>
|
||||||
@ -48,7 +48,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
|||||||
<p>
|
<p>
|
||||||
You need to{" "}
|
You need to{" "}
|
||||||
<Link href={`/?next=${currentPath}`}>
|
<Link href={`/?next=${currentPath}`}>
|
||||||
<a className="font-medium text-brand-base">Sign in</a>
|
<a className="font-medium text-custom-text-100">Sign in</a>
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
with an account that has access to this page.
|
with an account that has access to this page.
|
||||||
</p>
|
</p>
|
||||||
|
@ -41,13 +41,15 @@ export const JoinProject: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-brand-surface-1 text-center">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-100 text-center">
|
||||||
<div className="h-44 w-72">
|
<div className="h-44 w-72">
|
||||||
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
|
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-medium text-brand-base">You are not a member of this project</h1>
|
<h1 className="text-xl font-medium text-custom-text-100">
|
||||||
|
You are not a member of this project
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div className="w-full max-w-md text-base text-brand-secondary">
|
<div className="w-full max-w-md text-base text-custom-text-200">
|
||||||
<p className="mx-auto w-full text-sm md:w-3/4">
|
<p className="mx-auto w-full text-sm md:w-3/4">
|
||||||
You are not a member of this project, but you can join this project by clicking the button
|
You are not a member of this project, but you can join this project by clicking the button
|
||||||
below.
|
below.
|
||||||
|
@ -11,7 +11,7 @@ export const NotAWorkspaceMember = () => (
|
|||||||
<div className="space-y-8 text-center">
|
<div className="space-y-8 text-center">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-lg font-semibold">Not Authorized!</h3>
|
<h3 className="text-lg font-semibold">Not Authorized!</h3>
|
||||||
<p className="mx-auto w-1/2 text-sm text-brand-secondary">
|
<p className="mx-auto w-1/2 text-sm text-custom-text-200">
|
||||||
You{"'"}re not a member of this workspace. Please contact the workspace admin to get an
|
You{"'"}re not a member of this workspace. Please contact the workspace admin to get an
|
||||||
invitation or check your pending invitations.
|
invitation or check your pending invitations.
|
||||||
</p>
|
</p>
|
||||||
|
90
apps/app/components/automation/auto-archive-automation.tsx
Normal file
90
apps/app/components/automation/auto-archive-automation.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// component
|
||||||
|
import { CustomSelect, ToggleSwitch } from "components/ui";
|
||||||
|
import { SelectMonthModal } from "components/automation";
|
||||||
|
// icons
|
||||||
|
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
|
// constants
|
||||||
|
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||||
|
// types
|
||||||
|
import { IProject } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectDetails: IProject | undefined;
|
||||||
|
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
|
||||||
|
const [monthModal, setmonthModal] = useState(false);
|
||||||
|
|
||||||
|
const initialValues: Partial<IProject> = { archive_in: 1 };
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SelectMonthModal
|
||||||
|
type="auto-archive"
|
||||||
|
initialValues={initialValues}
|
||||||
|
isOpen={monthModal}
|
||||||
|
handleClose={() => setmonthModal(false)}
|
||||||
|
handleChange={handleChange}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-100 bg-custom-background-90">
|
||||||
|
<div className="flex items-center justify-between gap-x-8 gap-y-2">
|
||||||
|
<div className="flex flex-col gap-2.5">
|
||||||
|
<h4 className="text-lg font-semibold">Auto-archive closed issues</h4>
|
||||||
|
<p className="text-sm text-custom-text-200">
|
||||||
|
Plane will automatically archive issues that have been completed or cancelled for the
|
||||||
|
configured time period.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ToggleSwitch
|
||||||
|
value={projectDetails?.archive_in !== 0}
|
||||||
|
onChange={() =>
|
||||||
|
projectDetails?.archive_in === 0
|
||||||
|
? handleChange({ archive_in: 1 })
|
||||||
|
: handleChange({ archive_in: 0 })
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{projectDetails?.archive_in !== 0 && (
|
||||||
|
<div className="flex items-center justify-between gap-2 w-full">
|
||||||
|
<div className="w-1/2 text-base font-medium">
|
||||||
|
Auto-archive issues that are closed for
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<CustomSelect
|
||||||
|
value={projectDetails?.archive_in}
|
||||||
|
label={`${projectDetails?.archive_in} ${
|
||||||
|
projectDetails?.archive_in === 1 ? "Month" : "Months"
|
||||||
|
}`}
|
||||||
|
onChange={(val: number) => {
|
||||||
|
handleChange({ archive_in: val });
|
||||||
|
}}
|
||||||
|
input
|
||||||
|
verticalPosition="top"
|
||||||
|
width="w-full"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||||
|
<CustomSelect.Option key={month.label} value={month.value}>
|
||||||
|
{month.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
onClick={() => setmonthModal(true)}
|
||||||
|
>
|
||||||
|
Customize Time Range
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
</CustomSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
177
apps/app/components/automation/auto-close-automation.tsx
Normal file
177
apps/app/components/automation/auto-close-automation.tsx
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// component
|
||||||
|
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
|
||||||
|
import { SelectMonthModal } from "components/automation";
|
||||||
|
// icons
|
||||||
|
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
|
import { getStateGroupIcon } from "components/icons";
|
||||||
|
// services
|
||||||
|
import stateService from "services/state.service";
|
||||||
|
// constants
|
||||||
|
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||||
|
import { STATES_LIST } from "constants/fetch-keys";
|
||||||
|
// types
|
||||||
|
import { IProject } from "types";
|
||||||
|
// helper
|
||||||
|
import { getStatesList } from "helpers/state.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectDetails: IProject | undefined;
|
||||||
|
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
|
||||||
|
const [monthModal, setmonthModal] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { data: stateGroups } = useSWR(
|
||||||
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const states = getStatesList(stateGroups ?? {});
|
||||||
|
|
||||||
|
const options = states
|
||||||
|
?.filter((state) => state.group === "cancelled")
|
||||||
|
.map((state) => ({
|
||||||
|
value: state.id,
|
||||||
|
query: state.name,
|
||||||
|
content: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||||
|
{state.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const multipleOptions = options.length > 1;
|
||||||
|
|
||||||
|
const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null;
|
||||||
|
|
||||||
|
const selectedOption = states?.find(
|
||||||
|
(s) => s.id === projectDetails?.default_state ?? defaultState
|
||||||
|
);
|
||||||
|
const currentDefaultState = states.find((s) => s.id === defaultState);
|
||||||
|
|
||||||
|
const initialValues: Partial<IProject> = {
|
||||||
|
close_in: 1,
|
||||||
|
default_state: defaultState,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SelectMonthModal
|
||||||
|
type="auto-close"
|
||||||
|
initialValues={initialValues}
|
||||||
|
isOpen={monthModal}
|
||||||
|
handleClose={() => setmonthModal(false)}
|
||||||
|
handleChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-100 bg-custom-background-90">
|
||||||
|
<div className="flex items-center justify-between gap-x-8 gap-y-2 ">
|
||||||
|
<div className="flex flex-col gap-2.5">
|
||||||
|
<h4 className="text-lg font-semibold">Auto-close inactive issues</h4>
|
||||||
|
<p className="text-sm text-custom-text-200">
|
||||||
|
Plane will automatically close the issues that have not been updated for the
|
||||||
|
configured time period.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ToggleSwitch
|
||||||
|
value={projectDetails?.close_in !== 0}
|
||||||
|
onChange={() =>
|
||||||
|
projectDetails?.close_in === 0
|
||||||
|
? handleChange({ close_in: 1, default_state: defaultState })
|
||||||
|
: handleChange({ close_in: 0, default_state: null })
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{projectDetails?.close_in !== 0 && (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<div className="flex items-center justify-between gap-2 w-full">
|
||||||
|
<div className="w-1/2 text-base font-medium">
|
||||||
|
Auto-close issues that are inactive for
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<CustomSelect
|
||||||
|
value={projectDetails?.close_in}
|
||||||
|
label={`${projectDetails?.close_in} ${
|
||||||
|
projectDetails?.close_in === 1 ? "Month" : "Months"
|
||||||
|
}`}
|
||||||
|
onChange={(val: number) => {
|
||||||
|
handleChange({ close_in: val });
|
||||||
|
}}
|
||||||
|
input
|
||||||
|
width="w-full"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||||
|
<CustomSelect.Option key={month.label} value={month.value}>
|
||||||
|
{month.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
onClick={() => setmonthModal(true)}
|
||||||
|
>
|
||||||
|
Customize Time Range
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
</CustomSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-2 w-full">
|
||||||
|
<div className="w-1/2 text-base font-medium">Auto-close Status</div>
|
||||||
|
<div className="w-1/2 ">
|
||||||
|
<CustomSearchSelect
|
||||||
|
value={
|
||||||
|
projectDetails?.default_state ? projectDetails?.default_state : defaultState
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedOption ? (
|
||||||
|
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
|
||||||
|
) : currentDefaultState ? (
|
||||||
|
getStateGroupIcon(
|
||||||
|
currentDefaultState.group,
|
||||||
|
"16",
|
||||||
|
"16",
|
||||||
|
currentDefaultState.color
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
|
)}
|
||||||
|
{selectedOption?.name
|
||||||
|
? selectedOption.name
|
||||||
|
: currentDefaultState?.name ?? (
|
||||||
|
<span className="text-custom-text-200">State</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
handleChange({ default_state: val });
|
||||||
|
}}
|
||||||
|
options={options}
|
||||||
|
disabled={!multipleOptions}
|
||||||
|
width="w-full"
|
||||||
|
input
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
3
apps/app/components/automation/index.ts
Normal file
3
apps/app/components/automation/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./auto-close-automation";
|
||||||
|
export * from "./auto-archive-automation";
|
||||||
|
export * from "./select-month-modal";
|
147
apps/app/components/automation/select-month-modal.tsx
Normal file
147
apps/app/components/automation/select-month-modal.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
|
// types
|
||||||
|
import type { IProject } from "types";
|
||||||
|
|
||||||
|
// types
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
type: "auto-close" | "auto-archive";
|
||||||
|
initialValues: Partial<IProject>;
|
||||||
|
handleClose: () => void;
|
||||||
|
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectMonthModal: React.FC<Props> = ({
|
||||||
|
type,
|
||||||
|
initialValues,
|
||||||
|
isOpen,
|
||||||
|
handleClose,
|
||||||
|
handleChange,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
} = useForm<IProject>({
|
||||||
|
defaultValues: initialValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
reset(initialValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (formData: Partial<IProject>) => {
|
||||||
|
if (!workspaceSlug && !projectId) return;
|
||||||
|
handleChange(formData);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputSection = (name: string) => (
|
||||||
|
<div className="relative flex flex-col gap-1 justify-center w-full">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
placeholder="Enter Months"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
width="full"
|
||||||
|
validations={{
|
||||||
|
required: "Select a month between 1 and 12.",
|
||||||
|
min: 1,
|
||||||
|
max: 12,
|
||||||
|
}}
|
||||||
|
className="border-custom-border-200"
|
||||||
|
/>
|
||||||
|
<span className="absolute text-sm text-custom-text-200 top-2.5 right-8">Months</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-30" onClose={onClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.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-[#131313] bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.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 transform rounded-lg bg-custom-background-90 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div>
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||||
|
>
|
||||||
|
Customize Time Range
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-8 flex items-center gap-2">
|
||||||
|
<div className="flex w-full flex-col gap-1 justify-center">
|
||||||
|
{type === "auto-close" ? (
|
||||||
|
<>
|
||||||
|
{inputSection("close_in")}
|
||||||
|
{errors.close_in && (
|
||||||
|
<span className="text-sm px-1 text-red-500">
|
||||||
|
Select a month between 1 and 12.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{inputSection("archive_in")}
|
||||||
|
{errors.archive_in && (
|
||||||
|
<span className="text-sm px-1 text-red-500">
|
||||||
|
Select a month between 1 and 12.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex justify-end gap-2">
|
||||||
|
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Submitting..." : "Submit"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
@ -17,12 +17,12 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
|
className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-custom-sidebar-border-200 text-center text-sm hover:bg-custom-sidebar-background-90"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
iconName="keyboard_backspace"
|
iconName="keyboard_backspace"
|
||||||
className="text-base leading-4 text-brand-secondary group-hover:text-brand-base"
|
className="text-base leading-4 text-custom-sidebar-text-200 group-hover:text-custom-sidebar-text-100"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{children}
|
{children}
|
||||||
@ -41,7 +41,7 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
|
|||||||
<>
|
<>
|
||||||
{link ? (
|
{link ? (
|
||||||
<Link href={link}>
|
<Link href={link}>
|
||||||
<a className="border-r-2 border-brand-base px-3 text-sm">
|
<a className="border-r-2 border-custom-sidebar-border-200 px-3 text-sm">
|
||||||
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
|
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
|
||||||
{icon ?? null}
|
{icon ?? null}
|
||||||
{title}
|
{title}
|
||||||
|
@ -34,8 +34,8 @@ export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
{theme.label}
|
{theme.label}
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
@ -380,7 +380,6 @@ export const CommandPalette: React.FC = () => {
|
|||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={isIssueModalOpen}
|
isOpen={isIssueModalOpen}
|
||||||
handleClose={() => setIsIssueModalOpen(false)}
|
handleClose={() => setIsIssueModalOpen(false)}
|
||||||
@ -408,7 +407,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
|
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||||
@ -421,7 +420,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all">
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-custom-border-200 divide-opacity-10 rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
|
||||||
<Command
|
<Command
|
||||||
filter={(value, search) => {
|
filter={(value, search) => {
|
||||||
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
||||||
@ -444,7 +443,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{issueId && issueDetails && (
|
{issueId && issueDetails && (
|
||||||
<div className="flex p-3">
|
<div className="flex p-3">
|
||||||
<p className="overflow-hidden truncate rounded-md bg-brand-surface-1 p-1 px-2 text-xs font-medium text-brand-secondary">
|
<p className="overflow-hidden truncate rounded-md bg-custom-background-90 p-1 px-2 text-xs font-medium text-custom-text-200">
|
||||||
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
|
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
|
||||||
{issueDetails?.name}
|
{issueDetails?.name}
|
||||||
</p>
|
</p>
|
||||||
@ -452,11 +451,11 @@ export const CommandPalette: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-secondary"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<Command.Input
|
<Command.Input
|
||||||
className="w-full border-0 border-b border-brand-base bg-transparent p-4 pl-11 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
@ -470,7 +469,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
resultsCount === 0 &&
|
resultsCount === 0 &&
|
||||||
searchTerm !== "" &&
|
searchTerm !== "" &&
|
||||||
debouncedSearchTerm !== "" && (
|
debouncedSearchTerm !== "" && (
|
||||||
<div className="my-4 text-center text-brand-secondary">
|
<div className="my-4 text-center text-custom-text-200">
|
||||||
No results found.
|
No results found.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -533,9 +532,9 @@ export const CommandPalette: React.FC = () => {
|
|||||||
value={value}
|
value={value}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 overflow-hidden text-brand-secondary">
|
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
|
||||||
<Icon
|
<Icon
|
||||||
className="h-4 w-4 text-brand-secondary"
|
className="h-4 w-4 text-custom-text-200"
|
||||||
color="#6b7280"
|
color="#6b7280"
|
||||||
/>
|
/>
|
||||||
<p className="block flex-1 truncate">{item.name}</p>
|
<p className="block flex-1 truncate">{item.name}</p>
|
||||||
@ -562,8 +561,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
<Squares2X2Icon className="h-4 w-4 text-custom-text-200" />
|
||||||
Change state...
|
Change state...
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -575,8 +574,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
|
<ChartBarIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Change priority...
|
Change priority...
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -588,8 +587,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<UsersIcon className="h-4 w-4 text-brand-secondary" />
|
<UsersIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Assign to...
|
Assign to...
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -600,15 +599,15 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
{issueDetails?.assignees.includes(user.id) ? (
|
{issueDetails?.assignees.includes(user.id) ? (
|
||||||
<>
|
<>
|
||||||
<UserMinusIcon className="h-4 w-4 text-brand-secondary" />
|
<UserMinusIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Un-assign from me
|
Un-assign from me
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<UserPlusIcon className="h-4 w-4 text-brand-secondary" />
|
<UserPlusIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Assign to me
|
Assign to me
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -616,8 +615,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
|
||||||
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<TrashIcon className="h-4 w-4 text-brand-secondary" />
|
<TrashIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Delete issue
|
Delete issue
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -628,8 +627,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<LinkIcon className="h-4 w-4 text-brand-secondary" />
|
<LinkIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Copy issue URL to clipboard
|
Copy issue URL to clipboard
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -638,9 +637,9 @@ export const CommandPalette: React.FC = () => {
|
|||||||
<Command.Group heading="Issue">
|
<Command.Group heading="Issue">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={createNewIssue}
|
onSelect={createNewIssue}
|
||||||
className="focus:bg-brand-surface-2"
|
className="focus:bg-custom-background-80"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
|
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new issue
|
Create new issue
|
||||||
</div>
|
</div>
|
||||||
@ -654,7 +653,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={createNewProject}
|
onSelect={createNewProject}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
|
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new project
|
Create new project
|
||||||
</div>
|
</div>
|
||||||
@ -670,7 +669,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={createNewCycle}
|
onSelect={createNewCycle}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<ContrastIcon className="h-4 w-4" color="#6b7280" />
|
<ContrastIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new cycle
|
Create new cycle
|
||||||
</div>
|
</div>
|
||||||
@ -683,7 +682,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={createNewModule}
|
onSelect={createNewModule}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
|
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new module
|
Create new module
|
||||||
</div>
|
</div>
|
||||||
@ -693,7 +692,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
|
|
||||||
<Command.Group heading="View">
|
<Command.Group heading="View">
|
||||||
<Command.Item onSelect={createNewView} className="focus:outline-none">
|
<Command.Item onSelect={createNewView} className="focus:outline-none">
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<ViewListIcon className="h-4 w-4" color="#6b7280" />
|
<ViewListIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new view
|
Create new view
|
||||||
</div>
|
</div>
|
||||||
@ -703,7 +702,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
|
|
||||||
<Command.Group heading="Page">
|
<Command.Group heading="Page">
|
||||||
<Command.Item onSelect={createNewPage} className="focus:outline-none">
|
<Command.Item onSelect={createNewPage} className="focus:outline-none">
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<DocumentTextIcon className="h-4 w-4" color="#6b7280" />
|
<DocumentTextIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new page
|
Create new page
|
||||||
</div>
|
</div>
|
||||||
@ -721,7 +720,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}
|
}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<InboxIcon className="h-4 w-4" color="#6b7280" />
|
<InboxIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Open inbox
|
Open inbox
|
||||||
</div>
|
</div>
|
||||||
@ -740,7 +739,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<SettingIcon className="h-4 w-4" color="#6b7280" />
|
<SettingIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Search settings...
|
Search settings...
|
||||||
</div>
|
</div>
|
||||||
@ -751,8 +750,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={createNewWorkspace}
|
onSelect={createNewWorkspace}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<FolderPlusIcon className="h-4 w-4 text-brand-secondary" />
|
<FolderPlusIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Create new workspace
|
Create new workspace
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -764,8 +763,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Change interface theme...
|
Change interface theme...
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -781,8 +780,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<RocketLaunchIcon className="h-4 w-4 text-brand-secondary" />
|
<RocketLaunchIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Open keyboard shortcuts
|
Open keyboard shortcuts
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -793,8 +792,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<DocumentIcon className="h-4 w-4 text-brand-secondary" />
|
<DocumentIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Open Plane documentation
|
Open Plane documentation
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -805,7 +804,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<DiscordIcon className="h-4 w-4" color="#6b7280" />
|
<DiscordIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Join our Discord
|
Join our Discord
|
||||||
</div>
|
</div>
|
||||||
@ -820,7 +819,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<GithubIcon className="h-4 w-4" color="#6b7280" />
|
<GithubIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Report a bug
|
Report a bug
|
||||||
</div>
|
</div>
|
||||||
@ -832,8 +831,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-brand-secondary" />
|
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Chat with us
|
Chat with us
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -847,8 +846,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => redirect(`/${workspaceSlug}/settings`)}
|
onSelect={() => redirect(`/${workspaceSlug}/settings`)}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
General
|
General
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -856,8 +855,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
|
onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Members
|
Members
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -865,8 +864,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
|
onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Billing and Plans
|
Billing and Plans
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -874,8 +873,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
|
onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Integrations
|
Integrations
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -883,8 +882,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => redirect(`/${workspaceSlug}/settings/import-export`)}
|
onSelect={() => redirect(`/${workspaceSlug}/settings/import-export`)}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Import/Export
|
Import/Export
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
@ -85,29 +85,29 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
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 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-80 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||||
<div className="bg-brand-surface-2 p-5">
|
<div className="bg-custom-background-80 p-5">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
|
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as="h3"
|
||||||
className="flex justify-between text-lg font-medium leading-6 text-brand-base"
|
className="flex justify-between text-lg font-medium leading-6 text-custom-text-100"
|
||||||
>
|
>
|
||||||
<span>Keyboard Shortcuts</span>
|
<span>Keyboard Shortcuts</span>
|
||||||
<span>
|
<span>
|
||||||
<button type="button" onClick={() => setIsOpen(false)}>
|
<button type="button" onClick={() => setIsOpen(false)}>
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
className="h-6 w-6 text-gray-400 hover:text-brand-secondary"
|
className="h-6 w-6 text-custom-text-200 hover:text-custom-text-100"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-brand-base bg-brand-surface-1 px-3 py-2">
|
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-custom-border-200 bg-custom-background-90 px-3 py-2">
|
||||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-brand-secondary" />
|
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
<Input
|
<Input
|
||||||
className="w-full border-none bg-transparent py-1 px-2 text-xs text-brand-secondary focus:outline-none"
|
className="w-full border-none bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none"
|
||||||
id="search"
|
id="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
@ -123,22 +123,22 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
<div key={shortcut.keys} className="flex w-full flex-col">
|
<div key={shortcut.keys} className="flex w-full flex-col">
|
||||||
<div className="flex flex-col gap-y-3">
|
<div className="flex flex-col gap-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-brand-secondary">
|
<p className="text-sm text-custom-text-200">
|
||||||
{shortcut.description}
|
{shortcut.description}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-x-2.5">
|
<div className="flex items-center gap-x-2.5">
|
||||||
{shortcut.keys.split(",").map((key, index) => (
|
{shortcut.keys.split(",").map((key, index) => (
|
||||||
<span key={index} className="flex items-center gap-1">
|
<span key={index} className="flex items-center gap-1">
|
||||||
{key === "Ctrl" ? (
|
{key === "Ctrl" ? (
|
||||||
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5">
|
<span className="flex h-full items-center rounded-sm border border-custom-border-200 bg-custom-background-90 p-1.5">
|
||||||
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
<CommandIcon className="h-4 w-4 fill-current text-custom-text-200" />
|
||||||
</span>
|
</span>
|
||||||
) : key === "Ctrl" ? (
|
) : key === "Ctrl" ? (
|
||||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
|
<kbd className="rounded-sm border border-custom-border-200 bg-custom-background-90 p-1.5 text-sm font-medium text-custom-text-200">
|
||||||
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
<CommandIcon className="h-4 w-4 fill-current text-custom-text-200" />
|
||||||
</kbd>
|
</kbd>
|
||||||
) : (
|
) : (
|
||||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
|
<kbd className="rounded-sm border border-custom-border-200 bg-custom-background-90 px-2 py-1 text-sm font-medium text-custom-text-200">
|
||||||
{key}
|
{key}
|
||||||
</kbd>
|
</kbd>
|
||||||
)}
|
)}
|
||||||
@ -151,7 +151,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-y-3">
|
<div className="flex flex-col gap-y-3">
|
||||||
<p className="text-sm text-brand-secondary">
|
<p className="text-sm text-custom-text-200">
|
||||||
No shortcuts found for{" "}
|
No shortcuts found for{" "}
|
||||||
<span className="font-semibold italic">
|
<span className="font-semibold italic">
|
||||||
{`"`}
|
{`"`}
|
||||||
@ -168,20 +168,20 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
<div className="flex flex-col gap-y-3">
|
<div className="flex flex-col gap-y-3">
|
||||||
{shortcuts.map(({ keys, description }, index) => (
|
{shortcuts.map(({ keys, description }, index) => (
|
||||||
<div key={index} className="flex items-center justify-between">
|
<div key={index} className="flex items-center justify-between">
|
||||||
<p className="text-sm text-brand-secondary">{description}</p>
|
<p className="text-sm text-custom-text-200">{description}</p>
|
||||||
<div className="flex items-center gap-x-2.5">
|
<div className="flex items-center gap-x-2.5">
|
||||||
{keys.split(",").map((key, index) => (
|
{keys.split(",").map((key, index) => (
|
||||||
<span key={index} className="flex items-center gap-1">
|
<span key={index} className="flex items-center gap-1">
|
||||||
{key === "Ctrl" ? (
|
{key === "Ctrl" ? (
|
||||||
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-brand-secondary">
|
<span className="flex h-full items-center rounded-sm border border-custom-border-200 bg-custom-background-90 p-1.5 text-custom-text-200">
|
||||||
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
<CommandIcon className="h-4 w-4 fill-current text-custom-text-200" />
|
||||||
</span>
|
</span>
|
||||||
) : key === "Ctrl" ? (
|
) : key === "Ctrl" ? (
|
||||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
|
<kbd className="rounded-sm border border-custom-border-200 bg-custom-background-90 p-1.5 text-sm font-medium text-custom-text-200">
|
||||||
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
<CommandIcon className="h-4 w-4 fill-current text-custom-text-200" />
|
||||||
</kbd>
|
</kbd>
|
||||||
) : (
|
) : (
|
||||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
|
<kbd className="rounded-sm border border-custom-border-200 bg-custom-background-90 px-2 py-1 text-sm font-medium text-custom-text-200">
|
||||||
{key}
|
{key}
|
||||||
</kbd>
|
</kbd>
|
||||||
)}
|
)}
|
||||||
|
@ -74,7 +74,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!showEmptyGroups && (
|
{!showEmptyGroups && (
|
||||||
<div className="h-full w-96 flex-shrink-0 space-y-3 p-1">
|
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
|
||||||
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
||||||
@ -85,7 +85,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center justify-between gap-2 rounded bg-brand-surface-1 p-2 shadow"
|
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{currentState &&
|
{currentState &&
|
||||||
@ -96,7 +96,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
: addSpaceIfCamelCase(singleGroup)}
|
: addSpaceIfCamelCase(singleGroup)}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-brand-secondary">0</span>
|
<span className="text-xs text-custom-text-200">0</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -57,18 +57,6 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
let bgColor = "#000000";
|
|
||||||
if (selectedGroup === "state") bgColor = currentState?.color ?? "#000000";
|
|
||||||
|
|
||||||
if (selectedGroup === "priority")
|
|
||||||
groupTitle === "high"
|
|
||||||
? (bgColor = "#dc2626")
|
|
||||||
: groupTitle === "medium"
|
|
||||||
? (bgColor = "#f97316")
|
|
||||||
: groupTitle === "low"
|
|
||||||
? (bgColor = "#22c55e")
|
|
||||||
: (bgColor = "#ff0000");
|
|
||||||
|
|
||||||
const getGroupTitle = () => {
|
const getGroupTitle = () => {
|
||||||
let title = addSpaceIfCamelCase(groupTitle);
|
let title = addSpaceIfCamelCase(groupTitle);
|
||||||
|
|
||||||
@ -96,7 +84,8 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
|
|
||||||
switch (selectedGroup) {
|
switch (selectedGroup) {
|
||||||
case "state":
|
case "state":
|
||||||
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
|
icon =
|
||||||
|
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||||
break;
|
break;
|
||||||
case "priority":
|
case "priority":
|
||||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||||
@ -124,18 +113,18 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between px-1 ${
|
className={`flex items-center justify-between px-1 ${
|
||||||
!isCollapsed ? "flex-col rounded-md bg-brand-surface-1" : ""
|
!isCollapsed ? "flex-col rounded-md bg-custom-background-90" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
||||||
<div
|
<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" : ""
|
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center">{getGroupIcon()}</span>
|
<span className="flex items-center">{getGroupIcon()}</span>
|
||||||
<h2
|
<h2
|
||||||
className="text-lg font-semibold capitalize"
|
className="text-lg font-semibold capitalize truncate"
|
||||||
style={{
|
style={{
|
||||||
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
|
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
|
||||||
}}
|
}}
|
||||||
@ -145,7 +134,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
<span
|
<span
|
||||||
className={`${
|
className={`${
|
||||||
isCollapsed ? "ml-0.5" : ""
|
isCollapsed ? "ml-0.5" : ""
|
||||||
} min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs`}
|
} min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs`}
|
||||||
>
|
>
|
||||||
{groupedByIssues?.[groupTitle].length ?? 0}
|
{groupedByIssues?.[groupTitle].length ?? 0}
|
||||||
</span>
|
</span>
|
||||||
@ -155,7 +144,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
|
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
|
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCollapsed((prevData) => !prevData);
|
setIsCollapsed((prevData) => !prevData);
|
||||||
}}
|
}}
|
||||||
@ -169,7 +158,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
{!isCompleted && selectedGroup !== "created_by" && (
|
{!isCompleted && selectedGroup !== "created_by" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
|
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||||
onClick={addIssueToState}
|
onClick={addIssueToState}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
@ -60,6 +60,10 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
|
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
|
||||||
|
const issuesLength = groupedByIssues?.[groupTitle].length;
|
||||||
|
const hasMinimumNumberOfCards = issuesLength ? issuesLength >= 4 : false;
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -77,7 +81,9 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`relative h-full ${
|
className={`relative h-full ${
|
||||||
orderBy !== "sort_order" && snapshot.isDraggingOver ? "bg-brand-base/20" : ""
|
orderBy !== "sort_order" && snapshot.isDraggingOver
|
||||||
|
? "bg-custom-background-100/20"
|
||||||
|
: ""
|
||||||
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
|
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
@ -87,12 +93,12 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
snapshot.isDraggingOver ? "block" : "hidden"
|
snapshot.isDraggingOver ? "block" : "hidden"
|
||||||
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-brand-surface-1 opacity-50`}
|
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-custom-background-90 opacity-50`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
snapshot.isDraggingOver ? "block" : "hidden"
|
snapshot.isDraggingOver ? "block" : "hidden"
|
||||||
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-brand-base p-2 text-xs`}
|
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-custom-background-100 p-2 text-xs`}
|
||||||
>
|
>
|
||||||
This board is ordered by{" "}
|
This board is ordered by{" "}
|
||||||
{replaceUnderscoreIfSnakeCase(
|
{replaceUnderscoreIfSnakeCase(
|
||||||
@ -101,7 +107,11 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="pt-3 overflow-hidden overflow-y-scroll">
|
<div
|
||||||
|
className={`pt-3 ${
|
||||||
|
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||||
|
} `}
|
||||||
|
>
|
||||||
{groupedByIssues?.[groupTitle].map((issue, index) => (
|
{groupedByIssues?.[groupTitle].map((issue, index) => (
|
||||||
<Draggable
|
<Draggable
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
@ -150,7 +160,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
{type === "issue" ? (
|
{type === "issue" ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-brand-accent outline-none p-1"
|
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
||||||
onClick={addIssueToState}
|
onClick={addIssueToState}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@ -162,7 +172,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
customButton={
|
customButton={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-brand-accent outline-none"
|
className="flex items-center gap-2 font-medium text-custom-primary outline-none"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
Add Issue
|
||||||
|
@ -43,7 +43,7 @@ import {
|
|||||||
import { LayerDiagonalIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { handleIssuesMutation } from "constants/issue";
|
import { handleIssuesMutation } from "constants/issue";
|
||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
@ -265,8 +265,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</a>
|
</a>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div
|
<div
|
||||||
className={`mb-3 rounded bg-brand-base shadow ${
|
className={`mb-3 rounded bg-custom-background-90 shadow ${
|
||||||
snapshot.isDragging ? "border-2 border-brand-accent shadow-lg" : ""
|
snapshot.isDragging ? "border-2 border-custom-primary shadow-lg" : ""
|
||||||
}`}
|
}`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
@ -290,7 +290,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
<CustomMenu
|
<CustomMenu
|
||||||
customButton={
|
customButton={
|
||||||
<button
|
<button
|
||||||
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded p-1 text-left text-xs duration-300 hover:bg-brand-surface-2"
|
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded p-1 text-left text-xs duration-300 hover:bg-custom-background-80"
|
||||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||||
>
|
>
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
@ -330,13 +330,11 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
||||||
<a>
|
<a>
|
||||||
{properties.key && (
|
{properties.key && (
|
||||||
<div className="mb-2.5 text-xs font-medium text-brand-secondary">
|
<div className="mb-2.5 text-xs font-medium text-custom-text-200">
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h5 className="text-sm group-hover:text-brand-accent break-words line-clamp-3">
|
<h5 className="text-sm break-words line-clamp-3">{issue.name}</h5>
|
||||||
{issue.name}
|
|
||||||
</h5>
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="relative mt-2.5 flex flex-wrap items-center gap-2 text-xs">
|
<div className="relative mt-2.5 flex flex-wrap items-center gap-2 text-xs">
|
||||||
@ -358,7 +356,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
selfPositioned
|
selfPositioned
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.due_date && (
|
{properties.due_date && issue.target_date && (
|
||||||
<ViewDueDateSelect
|
<ViewDueDateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
@ -366,7 +364,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.labels && (
|
{properties.labels && issue.labels.length > 0 && (
|
||||||
<ViewLabelSelect
|
<ViewLabelSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
@ -384,7 +382,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
selfPositioned
|
selfPositioned
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.estimate && (
|
{properties.estimate && issue.estimate_point !== null && (
|
||||||
<ViewEstimateSelect
|
<ViewEstimateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
@ -393,30 +391,30 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
selfPositioned
|
selfPositioned
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.sub_issue_count && (
|
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
|
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
||||||
{issue.sub_issues_count}
|
{issue.sub_issues_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.link && (
|
{properties.link && issue.link_count > 0 && (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
|
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<LinkIcon className="h-3.5 w-3.5" />
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
{issue.link_count}
|
{issue.link_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.attachment_count && (
|
{properties.attachment_count && issue.attachment_count > 0 && (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
|
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
||||||
{issue.attachment_count}
|
{issue.attachment_count}
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,7 +62,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button>
|
<Popover.Button>
|
||||||
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-brand-base">
|
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-custom-text-100">
|
||||||
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
||||||
<span>{formatDate(currentDate, "yyyy")}</span>
|
<span>{formatDate(currentDate, "yyyy")}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -77,30 +77,30 @@ export const CalendarHeader: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-brand-surface-2 shadow-lg">
|
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-custom-background-80 shadow-lg">
|
||||||
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
||||||
{YEARS_LIST.map((year) => (
|
{YEARS_LIST.map((year) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
|
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
|
||||||
className={` ${
|
className={` ${
|
||||||
isSameYear(year.value, currentDate)
|
isSameYear(year.value, currentDate)
|
||||||
? "text-sm font-medium text-brand-base"
|
? "text-sm font-medium text-custom-text-100"
|
||||||
: "text-xs text-brand-secondary "
|
: "text-xs text-custom-text-200 "
|
||||||
} hover:text-sm hover:font-medium hover:text-brand-base`}
|
} hover:text-sm hover:font-medium hover:text-custom-text-100`}
|
||||||
>
|
>
|
||||||
{year.label}
|
{year.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 border-t border-brand-base px-2">
|
<div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
|
||||||
{MONTHS_LIST.map((month) => (
|
{MONTHS_LIST.map((month) => (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
updateDate(updateDateWithMonth(`${month.value}`, currentDate))
|
updateDate(updateDateWithMonth(`${month.value}`, currentDate))
|
||||||
}
|
}
|
||||||
className={`px-2 py-2 text-xs text-brand-secondary hover:font-medium hover:text-brand-base ${
|
className={`px-2 py-2 text-xs text-custom-text-200 hover:font-medium hover:text-custom-text-100 ${
|
||||||
isSameMonth(`${month.value}`, currentDate)
|
isSameMonth(`${month.value}`, currentDate)
|
||||||
? "font-medium text-brand-base"
|
? "font-medium text-custom-text-100"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -152,7 +152,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
|||||||
|
|
||||||
<div className="flex w-full items-center justify-end gap-2">
|
<div className="flex w-full items-center justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none"
|
className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMonthlyView) {
|
if (isMonthlyView) {
|
||||||
updateDate(new Date());
|
updateDate(new Date());
|
||||||
@ -170,7 +170,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
|||||||
|
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
customButton={
|
customButton={
|
||||||
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none ">
|
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none ">
|
||||||
{isMonthlyView ? "Monthly" : "Weekly"}
|
{isMonthlyView ? "Monthly" : "Weekly"}
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
@ -181,7 +181,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
|||||||
setIsMonthlyView(true);
|
setIsMonthlyView(true);
|
||||||
changeDateRange(startOfWeek(currentDate), lastDayOfWeek(currentDate));
|
changeDateRange(startOfWeek(currentDate), lastDayOfWeek(currentDate));
|
||||||
}}
|
}}
|
||||||
className="w-52 text-sm text-brand-secondary"
|
className="w-52 text-sm text-custom-text-200"
|
||||||
>
|
>
|
||||||
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
|
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
|
||||||
<span className="flex items-center gap-2">Monthly View</span>
|
<span className="flex items-center gap-2">Monthly View</span>
|
||||||
@ -198,7 +198,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
|||||||
getCurrentWeekEndDate(currentDate)
|
getCurrentWeekEndDate(currentDate)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="w-52 text-sm text-brand-secondary"
|
className="w-52 text-sm text-custom-text-200"
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
<span className="flex items-center gap-2">Weekly View</span>
|
<span className="flex items-center gap-2">Weekly View</span>
|
||||||
@ -207,7 +207,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary">
|
<div className="mt-1 flex w-52 items-center justify-between border-t border-custom-border-200 py-2 px-1 text-sm text-custom-text-200">
|
||||||
<h4>Show weekends</h4>
|
<h4>Show weekends</h4>
|
||||||
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
|
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -170,9 +170,9 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
||||||
|
|
||||||
return calendarIssues ? (
|
return calendarIssues ? (
|
||||||
<div className="h-full">
|
<div className="h-full overflow-y-auto">
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<div className="h-full rounded-lg p-8 text-brand-secondary">
|
<div className="h-full rounded-lg p-8 text-custom-text-200">
|
||||||
<CalendarHeader
|
<CalendarHeader
|
||||||
isMonthlyView={isMonthlyView}
|
isMonthlyView={isMonthlyView}
|
||||||
setIsMonthlyView={setIsMonthlyView}
|
setIsMonthlyView={setIsMonthlyView}
|
||||||
@ -191,7 +191,7 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
{weeks.map((date, index) => (
|
{weeks.map((date, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`flex items-center justify-start gap-2 border-brand-base bg-brand-surface-1 p-1.5 text-base font-medium text-brand-secondary ${
|
className={`flex items-center justify-start gap-2 border-custom-border-200 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200 ${
|
||||||
!isMonthlyView
|
!isMonthlyView
|
||||||
? showWeekEnds
|
? showWeekEnds
|
||||||
? (index + 1) % 7 === 0
|
? (index + 1) % 7 === 0
|
||||||
|
@ -49,7 +49,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
key={index}
|
key={index}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
|
className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-custom-border-200 p-2.5 text-left text-sm font-medium hover:bg-custom-background-90 ${
|
||||||
isMonthlyView ? "" : "pt-9"
|
isMonthlyView ? "" : "pt-9"
|
||||||
} ${
|
} ${
|
||||||
showWeekEnds
|
showWeekEnds
|
||||||
@ -83,7 +83,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
{totalIssues > 4 && (
|
{totalIssues > 4 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-min whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 px-1.5 py-1 text-xs"
|
className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 px-1.5 py-1 text-xs"
|
||||||
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
||||||
>
|
>
|
||||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||||
@ -91,13 +91,13 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-brand-surface-2 p-1 text-xs text-brand-secondary opacity-0 group-hover:opacity-100`}
|
className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-custom-background-80 p-1 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-center gap-1 text-center"
|
className="flex items-center justify-center gap-1 text-center"
|
||||||
onClick={() => addIssueToDate(date.date)}
|
onClick={() => addIssueToDate(date.date)}
|
||||||
>
|
>
|
||||||
<PlusSmallIcon className="h-4 w-4 text-brand-secondary" />
|
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Add issue
|
Add issue
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -163,8 +163,8 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
className={`w-full relative cursor-pointer rounded border border-brand-base px-1.5 py-1.5 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${
|
className={`w-full relative cursor-pointer rounded border border-custom-border-200 px-1.5 py-1.5 text-xs duration-300 hover:cursor-move hover:bg-custom-background-80 ${
|
||||||
snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
|
snapshot.isDragging ? "bg-custom-background-80 shadow-lg" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="group/card flex w-full flex-col items-start justify-center gap-1.5 text-xs sm:w-auto ">
|
<div className="group/card flex w-full flex-col items-start justify-center gap-1.5 text-xs sm:w-auto ">
|
||||||
@ -199,18 +199,18 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
tooltipHeading="Issue ID"
|
tooltipHeading="Issue ID"
|
||||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||||
>
|
>
|
||||||
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<span className="text-xs text-brand-base">{truncateText(issue.name, 25)}</span>
|
<span className="text-xs text-custom-text-100">{truncateText(issue.name, 25)}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{displayProperties && (
|
{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 && (
|
{properties.priority && (
|
||||||
<ViewPrioritySelect
|
<ViewPrioritySelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
@ -225,12 +225,13 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
position="left"
|
position="left"
|
||||||
|
className="max-w-full"
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{properties.due_date && (
|
{properties.due_date && issue.target_date && (
|
||||||
<ViewDueDateSelect
|
<ViewDueDateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
@ -238,7 +239,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.labels && (
|
{properties.labels && issue.labels.length > 0 && (
|
||||||
<ViewLabelSelect
|
<ViewLabelSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
@ -256,7 +257,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.estimate && (
|
{properties.estimate && issue.estimate_point !== null && (
|
||||||
<ViewEstimateSelect
|
<ViewEstimateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
@ -265,30 +266,30 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.sub_issue_count && (
|
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
|
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
||||||
{issue.sub_issues_count}
|
{issue.sub_issues_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.link && (
|
{properties.link && issue.link_count > 0 && (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<LinkIcon className="h-3.5 w-3.5" />
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
{issue.link_count}
|
{issue.link_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.attachment_count && (
|
{properties.attachment_count && issue.attachment_count > 0 && (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
||||||
{issue.attachment_count}
|
{issue.attachment_count}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,198 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
// hooks
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
// ui
|
|
||||||
import { PrimaryButton } from "components/ui";
|
|
||||||
import { ColorPickerInput } from "components/core";
|
|
||||||
// services
|
|
||||||
import userService from "services/user.service";
|
|
||||||
// helper
|
|
||||||
import { applyTheme } from "helpers/theme.helper";
|
|
||||||
// types
|
|
||||||
import { ICustomTheme } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
preLoadedData?: Partial<ICustomTheme> | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
|
||||||
const [darkPalette, setDarkPalette] = useState(false);
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
accent: preLoadedData?.accent ?? "#FE5050",
|
|
||||||
bgBase: preLoadedData?.bgBase ?? "#FFF7F7",
|
|
||||||
bgSurface1: preLoadedData?.bgSurface1 ?? "#FFE0E0",
|
|
||||||
bgSurface2: preLoadedData?.bgSurface2 ?? "#FFF7F7",
|
|
||||||
border: preLoadedData?.border ?? "#FFC9C9",
|
|
||||||
darkPalette: preLoadedData?.darkPalette ?? false,
|
|
||||||
palette: preLoadedData?.palette ?? "",
|
|
||||||
sidebar: preLoadedData?.sidebar ?? "#FFFFFF",
|
|
||||||
textBase: preLoadedData?.textBase ?? "#430000",
|
|
||||||
textSecondary: preLoadedData?.textSecondary ?? "#323232",
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
setValue,
|
|
||||||
reset,
|
|
||||||
} = useForm<any>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { setTheme } = useTheme();
|
|
||||||
const { mutateUser } = useUser();
|
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: any) => {
|
|
||||||
await userService
|
|
||||||
.updateUser({
|
|
||||||
theme: {
|
|
||||||
accent: formData.accent,
|
|
||||||
bgBase: formData.bgBase,
|
|
||||||
bgSurface1: formData.bgSurface1,
|
|
||||||
bgSurface2: formData.bgSurface2,
|
|
||||||
border: formData.border,
|
|
||||||
darkPalette: darkPalette,
|
|
||||||
palette: `${formData.bgBase},${formData.bgSurface1},${formData.bgSurface2},${formData.border},${formData.sidebar},${formData.accent},${formData.textBase},${formData.textSecondary}`,
|
|
||||||
sidebar: formData.sidebar,
|
|
||||||
textBase: formData.textBase,
|
|
||||||
textSecondary: formData.textSecondary,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
mutateUser((prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
return { ...prevData, user: res };
|
|
||||||
}, false);
|
|
||||||
applyTheme(formData.palette, darkPalette);
|
|
||||||
setTheme("custom");
|
|
||||||
})
|
|
||||||
.catch((err) => console.log(err));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateTheme = async (formData: any) => {
|
|
||||||
await handleFormSubmit({ ...formData, darkPalette });
|
|
||||||
|
|
||||||
reset({
|
|
||||||
...defaultValues,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reset({
|
|
||||||
...defaultValues,
|
|
||||||
...preLoadedData,
|
|
||||||
});
|
|
||||||
}, [preLoadedData, reset]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(handleUpdateTheme)}>
|
|
||||||
<div className="space-y-5">
|
|
||||||
<h3 className="text-lg font-semibold text-brand-base">Customize your theme</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
<h3 className="text-left text-base text-brand-secondary">Background</h3>
|
|
||||||
<ColorPickerInput
|
|
||||||
name="bgBase"
|
|
||||||
error={errors.bgBase}
|
|
||||||
watch={watch}
|
|
||||||
setValue={setValue}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
<h3 className="text-left text-base text-brand-secondary">Background surface 1</h3>
|
|
||||||
<ColorPickerInput
|
|
||||||
name="bgSurface1"
|
|
||||||
error={errors.bgSurface1}
|
|
||||||
watch={watch}
|
|
||||||
setValue={setValue}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
<h3 className="text-left text-base text-brand-secondary">Background surface 2</h3>
|
|
||||||
<ColorPickerInput
|
|
||||||
name="bgSurface2"
|
|
||||||
error={errors.bgSurface2}
|
|
||||||
watch={watch}
|
|
||||||
setValue={setValue}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
<h3 className="text-left text-base text-brand-secondary">Border</h3>
|
|
||||||
<ColorPickerInput
|
|
||||||
name="border"
|
|
||||||
error={errors.border}
|
|
||||||
watch={watch}
|
|
||||||
setValue={setValue}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
<h3 className="text-left text-base text-brand-secondary">Sidebar</h3>
|
|
||||||
<ColorPickerInput
|
|
||||||
name="sidebar"
|
|
||||||
error={errors.sidebar}
|
|
||||||
watch={watch}
|
|
||||||
setValue={setValue}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
<h3 className="text-left text-base text-brand-secondary">Accent</h3>
|
|
||||||
<ColorPickerInput
|
|
||||||
name="accent"
|
|
||||||
error={errors.accent}
|
|
||||||
watch={watch}
|
|
||||||
setValue={setValue}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
<h3 className="text-left text-base text-brand-secondary">Text primary</h3>
|
|
||||||
<ColorPickerInput
|
|
||||||
name="textBase"
|
|
||||||
error={errors.textBase}
|
|
||||||
watch={watch}
|
|
||||||
setValue={setValue}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
<h3 className="text-left text-base text-brand-secondary">Text secondary</h3>
|
|
||||||
<ColorPickerInput
|
|
||||||
name="textSecondary"
|
|
||||||
error={errors.textSecondary}
|
|
||||||
watch={watch}
|
|
||||||
setValue={setValue}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
|
||||||
{isSubmitting ? "Creating Theme..." : "Set Theme"}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
@ -5,21 +5,13 @@ import Link from "next/link";
|
|||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ArrowTopRightOnSquareIcon,
|
ArrowTopRightOnSquareIcon,
|
||||||
CalendarDaysIcon,
|
|
||||||
ChartBarIcon,
|
|
||||||
ChatBubbleBottomCenterTextIcon,
|
|
||||||
ChatBubbleLeftEllipsisIcon,
|
ChatBubbleLeftEllipsisIcon,
|
||||||
LinkIcon,
|
|
||||||
PaperClipIcon,
|
|
||||||
PlayIcon,
|
|
||||||
RectangleGroupIcon,
|
|
||||||
Squares2X2Icon,
|
Squares2X2Icon,
|
||||||
TrashIcon,
|
|
||||||
UserIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
|
import { BlockedIcon, BlockerIcon } from "components/icons";
|
||||||
|
import { Icon } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import RemirrorRichTextEditor from "components/rich-text-editor";
|
import RemirrorRichTextEditor from "components/rich-text-editor";
|
||||||
@ -32,11 +24,11 @@ const activityDetails: {
|
|||||||
} = {
|
} = {
|
||||||
assignee: {
|
assignee: {
|
||||||
message: "removed the assignee",
|
message: "removed the assignee",
|
||||||
icon: <UserGroupIcon className="h-3 w-3" color="#6b7280" aria-hidden="true" />,
|
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
assignees: {
|
assignees: {
|
||||||
message: "added a new assignee",
|
message: "added a new assignee",
|
||||||
icon: <UserGroupIcon className="h-3 w-3" color="#6b7280" aria-hidden="true" />,
|
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
blocks: {
|
blocks: {
|
||||||
message: "marked this issue being blocked by",
|
message: "marked this issue being blocked by",
|
||||||
@ -48,62 +40,62 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
cycles: {
|
cycles: {
|
||||||
message: "set the cycle to",
|
message: "set the cycle to",
|
||||||
icon: <CyclesIcon height="12" width="12" color="#6b7280" />,
|
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
icon: <TagIcon height="12" width="12" color="#6b7280" />,
|
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
message: "set the module to",
|
message: "set the module to",
|
||||||
icon: <RectangleGroupIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
message: "set the state to",
|
message: "set the state to",
|
||||||
icon: <Squares2X2Icon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
icon: <Squares2X2Icon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
priority: {
|
priority: {
|
||||||
message: "set the priority to",
|
message: "set the priority to",
|
||||||
icon: <ChartBarIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
message: "set the name to",
|
message: "set the name to",
|
||||||
icon: (
|
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||||
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
message: "updated the description.",
|
message: "updated the description.",
|
||||||
icon: (
|
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||||
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
estimate_point: {
|
estimate_point: {
|
||||||
message: "set the estimate point to",
|
message: "set the estimate point to",
|
||||||
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-brand-secondary" aria-hidden="true" />,
|
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
message: "set the due date to",
|
message: "set the due date to",
|
||||||
icon: <CalendarDaysIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
parent: {
|
parent: {
|
||||||
message: "set the parent to",
|
message: "set the parent to",
|
||||||
icon: <UserIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
issue: {
|
issue: {
|
||||||
message: "deleted the issue.",
|
message: "deleted the issue.",
|
||||||
icon: <TrashIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
icon: <Icon iconName="delete" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
estimate: {
|
estimate: {
|
||||||
message: "updated the estimate",
|
message: "updated the estimate",
|
||||||
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
|
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
message: "updated the link",
|
message: "updated the link",
|
||||||
icon: <LinkIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
message: "updated the attachment",
|
message: "updated the attachment",
|
||||||
icon: <PaperClipIcon className="h-3 w-3 text-gray-500 " aria-hidden="true" />,
|
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
|
||||||
|
},
|
||||||
|
archived_at: {
|
||||||
|
message: "archived",
|
||||||
|
icon: <Icon iconName="archive" className="!text-sm text-custom-text-200" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -144,6 +136,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
action = `${activity.verb} the`;
|
action = `${activity.verb} the`;
|
||||||
} else if (activity.field === "link") {
|
} else if (activity.field === "link") {
|
||||||
action = `${activity.verb} the`;
|
action = `${activity.verb} the`;
|
||||||
|
} else if (activity.field === "archived_at") {
|
||||||
|
action =
|
||||||
|
activity.new_value && activity.new_value === "restore"
|
||||||
|
? "restored the issue"
|
||||||
|
: "archived the issue";
|
||||||
}
|
}
|
||||||
// for values that are after the action clause
|
// for values that are after the action clause
|
||||||
let value: any = activity.new_value ? activity.new_value : activity.old_value;
|
let value: any = activity.new_value ? activity.new_value : activity.old_value;
|
||||||
@ -157,7 +154,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
) {
|
) {
|
||||||
const { workspace_detail, project, issue } = activity;
|
const { workspace_detail, project, issue } = activity;
|
||||||
value = (
|
value = (
|
||||||
<span className="text-brand-secondary">
|
<span className="text-custom-text-200">
|
||||||
created{" "}
|
created{" "}
|
||||||
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
|
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
|
||||||
<a className="inline-flex items-center hover:underline">
|
<a className="inline-flex items-center hover:underline">
|
||||||
@ -187,7 +184,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
activity.new_value && activity.new_value !== ""
|
activity.new_value && activity.new_value !== ""
|
||||||
? activity.new_value
|
? activity.new_value
|
||||||
: activity.old_value;
|
: activity.old_value;
|
||||||
value = renderShortNumericDateFormat(date as string);
|
value = renderShortDateWithYearFormat(date as string);
|
||||||
} else if (activity.field === "description") {
|
} else if (activity.field === "description") {
|
||||||
value = "description";
|
value = "description";
|
||||||
} else if (activity.field === "attachment") {
|
} else if (activity.field === "attachment") {
|
||||||
@ -205,7 +202,13 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
<div key={activity.id} className="mt-2">
|
<div key={activity.id} className="mt-2">
|
||||||
<div className="relative flex items-start space-x-3">
|
<div className="relative flex items-start space-x-3">
|
||||||
<div className="relative px-1">
|
<div className="relative px-1">
|
||||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
{activity.field ? (
|
||||||
|
activity.new_value === "restore" ? (
|
||||||
|
<Icon iconName="history" className="text-sm text-custom-text-200" />
|
||||||
|
) : (
|
||||||
|
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||||
|
)
|
||||||
|
) : activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||||
<img
|
<img
|
||||||
src={activity.actor_detail.avatar}
|
src={activity.actor_detail.avatar}
|
||||||
alt={activity.actor_detail.first_name}
|
alt={activity.actor_detail.first_name}
|
||||||
@ -221,9 +224,9 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px">
|
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||||
<ChatBubbleLeftEllipsisIcon
|
<ChatBubbleLeftEllipsisIcon
|
||||||
className="h-3.5 w-3.5 text-brand-secondary"
|
className="h-3.5 w-3.5 text-custom-text-200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@ -234,7 +237,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
{activity.actor_detail.first_name}
|
{activity.actor_detail.first_name}
|
||||||
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
|
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-xs text-brand-secondary">
|
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||||
Commented {timeAgo(activity.created_at)}
|
Commented {timeAgo(activity.created_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -247,7 +250,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
}
|
}
|
||||||
editable={false}
|
editable={false}
|
||||||
noBorder
|
noBorder
|
||||||
customClassName="text-xs border border-brand-base bg-brand-base"
|
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -262,7 +265,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
<div className="relative pb-1">
|
<div className="relative pb-1">
|
||||||
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
|
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
|
||||||
<span
|
<span
|
||||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-brand-surface-2"
|
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@ -271,7 +274,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
<div>
|
<div>
|
||||||
<div className="relative px-1.5">
|
<div className="relative px-1.5">
|
||||||
<div className="mt-1.5">
|
<div className="mt-1.5">
|
||||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-2 ring-white">
|
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||||
{activity.field ? (
|
{activity.field ? (
|
||||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||||
) : activity.actor_detail.avatar &&
|
) : activity.actor_detail.avatar &&
|
||||||
@ -295,15 +298,24 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 py-3">
|
<div className="min-w-0 flex-1 py-3">
|
||||||
<div className="text-xs text-brand-secondary">
|
<div className="text-xs text-custom-text-200">
|
||||||
<span className="text-gray font-medium">
|
{activity.field === "archived_at" && activity.new_value !== "restore" ? (
|
||||||
{activity.actor_detail.first_name}
|
<span className="text-gray font-medium">Plane</span>
|
||||||
{activity.actor_detail.is_bot
|
) : (
|
||||||
? " Bot"
|
<span className="text-gray font-medium">
|
||||||
: " " + activity.actor_detail.last_name}
|
{activity.actor_detail.first_name}
|
||||||
</span>
|
{activity.actor_detail.is_bot
|
||||||
|
? " Bot"
|
||||||
|
: " " + activity.actor_detail.last_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span> {action} </span>
|
<span> {action} </span>
|
||||||
<span className="text-xs font-medium text-brand-base"> {value} </span>
|
{activity.field !== "archived_at" && (
|
||||||
|
<span className="text-xs font-medium text-custom-text-100">
|
||||||
|
{" "}
|
||||||
|
{value}{" "}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
|
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
186
apps/app/components/core/filters/due-date-filter-modal.tsx
Normal file
186
apps/app/components/core/filters/due-date-filter-modal.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
// react-datepicker
|
||||||
|
import DatePicker from "react-datepicker";
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// hooks
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
|
// components
|
||||||
|
import { DueDateFilterSelect } from "./due-date-filter-select";
|
||||||
|
// ui
|
||||||
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||||
|
// helpers
|
||||||
|
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TFormValues = {
|
||||||
|
filterType: "before" | "after" | "range";
|
||||||
|
date1: Date;
|
||||||
|
date2: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues: TFormValues = {
|
||||||
|
filterType: "range",
|
||||||
|
date1: new Date(),
|
||||||
|
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) => {
|
||||||
|
const { filters, setFilters } = useIssuesView();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { viewId } = router.query;
|
||||||
|
|
||||||
|
const { handleSubmit, watch, control } = useForm<TFormValues>({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormSubmit = (formData: TFormValues) => {
|
||||||
|
const { filterType, date1, date2 } = formData;
|
||||||
|
|
||||||
|
if (filterType === "range") {
|
||||||
|
setFilters(
|
||||||
|
{ target_date: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
|
||||||
|
!Boolean(viewId)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const filteredArray = filters?.target_date?.filter((item) => {
|
||||||
|
if (item?.includes(filterType)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
|
||||||
|
if (filterOne)
|
||||||
|
setFilters(
|
||||||
|
{ target_date: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
|
||||||
|
!Boolean(viewId)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
setFilters(
|
||||||
|
{
|
||||||
|
target_date: [`${renderDateFormat(date1)};${filterType}`],
|
||||||
|
},
|
||||||
|
!Boolean(viewId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInvalid =
|
||||||
|
watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
|
||||||
|
|
||||||
|
const nextDay = new Date(watch("date1"));
|
||||||
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-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-custom-border-200 bg-custom-background-100 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-custom-text-200">After:</span>
|
||||||
|
<span>{renderShortDateWithYearFormat(watch("date1"))}</span>
|
||||||
|
<span className="text-custom-text-200 ml-1">Before:</span>
|
||||||
|
{!isInvalid && <span>{renderShortDateWithYearFormat(watch("date2"))}</span>}
|
||||||
|
</h6>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<SecondaryButton className="flex items-center gap-2" onClick={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
type="submit"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
disabled={isInvalid}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
58
apps/app/components/core/filters/due-date-filter-select.tsx
Normal file
58
apps/app/components/core/filters/due-date-filter-select.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// ui
|
||||||
|
import { CustomSelect } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { CalendarBeforeIcon, CalendarAfterIcon, CalendarMonthIcon } from "components/icons";
|
||||||
|
// fetch-keys
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DueDate = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
icon: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dueDateRange: DueDate[] = [
|
||||||
|
{
|
||||||
|
name: "Due date before",
|
||||||
|
value: "before",
|
||||||
|
icon: <CalendarBeforeIcon className="h-4 w-4 " />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Due date after",
|
||||||
|
value: "after",
|
||||||
|
icon: <CalendarAfterIcon className="h-4 w-4 " />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Due date range",
|
||||||
|
value: "range",
|
||||||
|
icon: <CalendarMonthIcon className="h-4 w-4 " />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
|
||||||
|
<CustomSelect
|
||||||
|
value={value}
|
||||||
|
label={
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
{dueDateRange.find((item) => item.value === value)?.icon}
|
||||||
|
<span>{dueDateRange.find((item) => item.value === value)?.name}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
{dueDateRange.map((option, index) => (
|
||||||
|
<CustomSelect.Option key={index} value={option.value}>
|
||||||
|
<>
|
||||||
|
<span>{option.icon}</span>
|
||||||
|
{option.name}
|
||||||
|
</>
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
);
|
@ -17,6 +17,7 @@ import stateService from "services/state.service";
|
|||||||
// types
|
// types
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
|
||||||
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -57,10 +58,10 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1"
|
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
|
||||||
>
|
>
|
||||||
<span className="capitalize text-brand-secondary">
|
<span className="capitalize text-custom-text-200">
|
||||||
{replaceUnderscoreIfSnakeCase(key)}:
|
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||||
</span>
|
</span>
|
||||||
{filters[key as keyof IIssueFilterOptions] === null ||
|
{filters[key as keyof IIssueFilterOptions] === null ||
|
||||||
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
|
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
|
||||||
@ -131,7 +132,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
? "bg-yellow-500/20 text-yellow-500"
|
? "bg-yellow-500/20 text-yellow-500"
|
||||||
: priority === "low"
|
: priority === "low"
|
||||||
? "bg-green-500/20 text-green-500"
|
? "bg-green-500/20 text-green-500"
|
||||||
: "bg-brand-surface-1 text-brand-secondary"
|
: "bg-custom-background-90 text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>{getPriorityIcon(priority)}</span>
|
<span>{getPriorityIcon(priority)}</span>
|
||||||
@ -170,7 +171,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={memberId}
|
key={memberId}
|
||||||
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
|
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
|
||||||
>
|
>
|
||||||
<Avatar user={member} />
|
<Avatar user={member} />
|
||||||
<span>{member?.first_name}</span>
|
<span>{member?.first_name}</span>
|
||||||
@ -211,7 +212,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${memberId}-${key}`}
|
key={`${memberId}-${key}`}
|
||||||
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
|
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
|
||||||
>
|
>
|
||||||
<Avatar user={member} />
|
<Avatar user={member} />
|
||||||
<span>{member?.first_name}</span>
|
<span>{member?.first_name}</span>
|
||||||
@ -299,6 +300,51 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
<XMarkIcon className="h-3 w-3" />
|
<XMarkIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : key === "target_date" ? (
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{filters.target_date?.map((date: string) => {
|
||||||
|
if (filters.target_date.length <= 0) return null;
|
||||||
|
|
||||||
|
const splitDate = date.split(";");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={date}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
|
||||||
|
>
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full" />
|
||||||
|
<span className="capitalize">
|
||||||
|
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters(
|
||||||
|
{
|
||||||
|
target_date: filters.target_date?.filter(
|
||||||
|
(d: any) => d !== date
|
||||||
|
),
|
||||||
|
},
|
||||||
|
!Boolean(viewId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
target_date: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
(filters[key as keyof IIssueFilterOptions] as any)?.join(", ")
|
(filters[key as keyof IIssueFilterOptions] as any)?.join(", ")
|
||||||
)}
|
)}
|
||||||
@ -332,9 +378,10 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
assignees: null,
|
assignees: null,
|
||||||
labels: null,
|
labels: null,
|
||||||
created_by: null,
|
created_by: null,
|
||||||
|
target_date: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs"
|
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
|
||||||
>
|
>
|
||||||
<span>Clear all filters</span>
|
<span>Clear all filters</span>
|
||||||
<XMarkIcon className="h-3 w-3" />
|
<XMarkIcon className="h-3 w-3" />
|
4
apps/app/components/core/filters/index.ts
Normal file
4
apps/app/components/core/filters/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./due-date-filter-modal";
|
||||||
|
export * from "./due-date-filter-select";
|
||||||
|
export * from "./filters-list";
|
||||||
|
export * from "./issues-view-filter";
|
@ -2,34 +2,60 @@ import React from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// headless ui
|
import useEstimateOption from "hooks/use-estimate-option";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
|
||||||
// components
|
// components
|
||||||
import { SelectFilters } from "components/views";
|
import { SelectFilters } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Icon, ToggleSwitch } from "components/ui";
|
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
|
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
CalendarMonthOutlined,
|
||||||
ListBulletIcon,
|
FormatListBulletedOutlined,
|
||||||
Squares2X2Icon,
|
GridViewOutlined,
|
||||||
CalendarDaysIcon,
|
TableChartOutlined,
|
||||||
ChartBarIcon,
|
WaterfallChartOutlined,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@mui/icons-material";
|
||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { Properties } from "types";
|
import { Properties, TIssueViewOptions } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||||
import useEstimateOption from "hooks/use-estimate-option";
|
|
||||||
|
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
Icon: FormatListBulletedOutlined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "kanban",
|
||||||
|
Icon: GridViewOutlined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "calendar",
|
||||||
|
Icon: CalendarMonthOutlined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "spreadsheet",
|
||||||
|
Icon: TableChartOutlined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "gantt_chart",
|
||||||
|
Icon: WaterfallChartOutlined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const IssuesFilterView: React.FC = () => {
|
export const IssuesFilterView: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
issueView,
|
issueView,
|
||||||
@ -55,80 +81,69 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-x-1">
|
{!isArchivedIssues && (
|
||||||
<button
|
<div className="flex items-center gap-x-1">
|
||||||
type="button"
|
{issueViewOptions.map((option) => (
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
<Tooltip
|
||||||
issueView === "list" ? "bg-brand-surface-2" : ""
|
key={option.type}
|
||||||
}`}
|
tooltipContent={
|
||||||
onClick={() => setIssueView("list")}
|
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||||
>
|
}
|
||||||
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
|
position="bottom"
|
||||||
</button>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||||
issueView === "kanban" ? "bg-brand-surface-2" : ""
|
issueView === option.type
|
||||||
}`}
|
? "bg-custom-sidebar-background-80"
|
||||||
onClick={() => setIssueView("kanban")}
|
: "text-custom-sidebar-text-200"
|
||||||
>
|
}`}
|
||||||
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
onClick={() => setIssueView(option.type)}
|
||||||
</button>
|
>
|
||||||
<button
|
<option.Icon
|
||||||
type="button"
|
sx={{
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
fontSize: 16,
|
||||||
issueView === "calendar" ? "bg-brand-surface-2" : ""
|
}}
|
||||||
}`}
|
className={option.type === "gantt_chart" ? "rotate-90" : ""}
|
||||||
onClick={() => setIssueView("calendar")}
|
/>
|
||||||
>
|
</button>
|
||||||
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
|
</Tooltip>
|
||||||
</button>
|
))}
|
||||||
<button
|
</div>
|
||||||
type="button"
|
)}
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
|
||||||
issueView === "spreadsheet" ? "bg-brand-surface-2" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setIssueView("spreadsheet")}
|
|
||||||
>
|
|
||||||
<Icon iconName="table_chart" className="text-brand-secondary" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`grid h-7 w-7 place-items-center rounded outline-none duration-300 hover:bg-brand-surface-2 ${
|
|
||||||
issueView === "gantt_chart" ? "bg-brand-surface-2" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setIssueView("gantt_chart")}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-rounded text-brand-secondary text-[18px] rotate-90">
|
|
||||||
waterfall_chart
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<SelectFilters
|
<SelectFilters
|
||||||
filters={filters}
|
filters={filters}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
const key = option.key as keyof typeof filters;
|
const key = option.key as keyof typeof filters;
|
||||||
|
|
||||||
const valueExists = filters[key]?.includes(option.value);
|
if (key === "target_date") {
|
||||||
|
const valueExists = checkIfArraysHaveSameElements(
|
||||||
|
filters.target_date ?? [],
|
||||||
|
option.value
|
||||||
|
);
|
||||||
|
|
||||||
if (valueExists) {
|
setFilters({
|
||||||
setFilters(
|
target_date: valueExists ? null : option.value,
|
||||||
{
|
});
|
||||||
...(filters ?? {}),
|
|
||||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
|
||||||
(val) => val !== option.value
|
|
||||||
),
|
|
||||||
},
|
|
||||||
!Boolean(viewId)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setFilters(
|
const valueExists = filters[key]?.includes(option.value);
|
||||||
{
|
|
||||||
...(filters ?? {}),
|
if (valueExists)
|
||||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
setFilters(
|
||||||
},
|
{
|
||||||
!Boolean(viewId)
|
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||||
);
|
(val) => val !== option.value
|
||||||
|
),
|
||||||
|
},
|
||||||
|
!Boolean(viewId)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
setFilters(
|
||||||
|
{
|
||||||
|
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||||
|
},
|
||||||
|
!Boolean(viewId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
direction="left"
|
direction="left"
|
||||||
@ -138,8 +153,10 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex items-center gap-2 rounded-md border border-brand-base bg-transparent px-3 py-1.5 text-xs hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none ${
|
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||||
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
|
open
|
||||||
|
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
||||||
|
: "text-custom-sidebar-text-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
@ -155,19 +172,18 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-brand-base bg-brand-surface-1 p-3 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||||
<div className="relative divide-y-2 divide-brand-base">
|
<div className="relative divide-y-2 divide-custom-border-200">
|
||||||
<div className="space-y-4 pb-3 text-xs">
|
<div className="space-y-4 pb-3 text-xs">
|
||||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-brand-secondary">Group by</h4>
|
<h4 className="text-custom-text-200">Group by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||||
?.name ?? "Select"
|
?.name ?? "Select"
|
||||||
}
|
}
|
||||||
width="lg"
|
|
||||||
>
|
>
|
||||||
{GROUP_BY_OPTIONS.map((option) =>
|
{GROUP_BY_OPTIONS.map((option) =>
|
||||||
issueView === "kanban" && option.key === null ? null : (
|
issueView === "kanban" && option.key === null ? null : (
|
||||||
@ -182,13 +198,12 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-brand-secondary">Order by</h4>
|
<h4 className="text-custom-text-200">Order by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||||
"Select"
|
"Select"
|
||||||
}
|
}
|
||||||
width="lg"
|
|
||||||
>
|
>
|
||||||
{ORDER_BY_OPTIONS.map((option) =>
|
{ORDER_BY_OPTIONS.map((option) =>
|
||||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||||
@ -207,13 +222,12 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-brand-secondary">Issue type</h4>
|
<h4 className="text-custom-text-200">Issue type</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||||
?.name ?? "Select"
|
?.name ?? "Select"
|
||||||
}
|
}
|
||||||
width="lg"
|
|
||||||
>
|
>
|
||||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
@ -233,7 +247,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-brand-secondary">Show empty states</h4>
|
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={showEmptyGroups}
|
value={showEmptyGroups}
|
||||||
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
|
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||||
@ -245,7 +259,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="font-medium text-brand-accent"
|
className="font-medium text-custom-primary"
|
||||||
onClick={() => setNewFilterDefaultView()}
|
onClick={() => setNewFilterDefaultView()}
|
||||||
>
|
>
|
||||||
Set as default
|
Set as default
|
||||||
@ -256,15 +270,22 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 py-3">
|
<div className="space-y-2 py-3">
|
||||||
<h4 className="text-sm text-brand-secondary">Display Properties</h4>
|
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{Object.keys(properties).map((key) => {
|
{Object.keys(properties).map((key) => {
|
||||||
if (key === "estimate" && !isEstimateActive) return null;
|
if (key === "estimate" && !isEstimateActive) return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(issueView === "spreadsheet" && key === "sub_issue_count") ||
|
issueView === "spreadsheet" &&
|
||||||
key === "attachment_count" ||
|
(key === "attachment_count" ||
|
||||||
key === "link"
|
key === "link" ||
|
||||||
|
key === "sub_issue_count")
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
issueView !== "spreadsheet" &&
|
||||||
|
(key === "created_on" || key === "updated_on")
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -274,8 +295,8 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||||
properties[key as keyof Properties]
|
properties[key as keyof Properties]
|
||||||
? "border-brand-accent bg-brand-accent text-white"
|
? "border-custom-primary bg-custom-primary text-white"
|
||||||
: "border-brand-base"
|
: "border-custom-border-200"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
>
|
>
|
@ -62,7 +62,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
|||||||
return (
|
return (
|
||||||
<Popover className="relative z-[2]" ref={ref}>
|
<Popover className="relative z-[2]" ref={ref}>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="rounded-md border border-brand-base bg-brand-surface-2 px-2 py-1 text-xs text-brand-secondary"
|
className="rounded-md border border-custom-border-200 bg-custom-background-80 px-2 py-1 text-xs text-custom-text-200"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@ -76,16 +76,16 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-brand-base bg-brand-surface-2 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
||||||
<div className="h-96 w-80 overflow-auto rounded border border-brand-base bg-brand-surface-2 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
<div className="h-96 w-80 overflow-auto rounded border border-custom-border-200 bg-custom-background-80 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List as="span" className="inline-block rounded bg-brand-surface-2 p-1">
|
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
||||||
{tabOptions.map((tab) => (
|
{tabOptions.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`rounded py-1 px-4 text-center text-sm outline-none transition-colors ${
|
`rounded py-1 px-4 text-center text-sm outline-none transition-colors ${
|
||||||
selected ? "bg-brand-accent text-white" : "text-brand-base"
|
selected ? "bg-custom-primary text-white" : "text-custom-text-100"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -1,19 +1,12 @@
|
|||||||
export * from "./board-view";
|
export * from "./board-view";
|
||||||
export * from "./calendar-view";
|
export * from "./calendar-view";
|
||||||
|
export * from "./filters";
|
||||||
export * from "./gantt-chart-view";
|
export * from "./gantt-chart-view";
|
||||||
export * from "./list-view";
|
export * from "./list-view";
|
||||||
|
export * from "./modals";
|
||||||
export * from "./spreadsheet-view";
|
export * from "./spreadsheet-view";
|
||||||
|
export * from "./theme";
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./bulk-delete-issues-modal";
|
|
||||||
export * from "./existing-issues-list-modal";
|
|
||||||
export * from "./filters-list";
|
|
||||||
export * from "./gpt-assistant-modal";
|
|
||||||
export * from "./image-upload-modal";
|
|
||||||
export * from "./issues-view-filter";
|
|
||||||
export * from "./issues-view";
|
export * from "./issues-view";
|
||||||
export * from "./link-modal";
|
|
||||||
export * from "./image-picker-popover";
|
export * from "./image-picker-popover";
|
||||||
export * from "./feeds";
|
export * from "./feeds";
|
||||||
export * from "./theme-switch";
|
|
||||||
export * from "./custom-theme-selector";
|
|
||||||
export * from "./color-picker-input";
|
|
||||||
|
@ -29,19 +29,14 @@ import {
|
|||||||
} from "components/core";
|
} from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
import { CycleIssuesGanttChartView, TransferIssues, TransferIssuesModal } from "components/cycles";
|
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||||
import { IssueGanttChartView } from "components/issues/gantt-chart";
|
|
||||||
// ui
|
// ui
|
||||||
import { EmptySpace, EmptySpaceItem, EmptyState, PrimaryButton, Spinner } from "components/ui";
|
import { EmptyState, PrimaryButton, Spinner, Icon } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
ListBulletIcon,
|
|
||||||
PlusIcon,
|
|
||||||
RectangleStackIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
// images
|
// images
|
||||||
import emptyIssue from "public/empty-state/empty-issue.svg";
|
import emptyIssue from "public/empty-state/issue.svg";
|
||||||
|
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
|
||||||
// helpers
|
// helpers
|
||||||
import { getStatesList } from "helpers/state.helper";
|
import { getStatesList } from "helpers/state.helper";
|
||||||
import { orderArrayBy } from "helpers/array.helper";
|
import { orderArrayBy } from "helpers/array.helper";
|
||||||
@ -56,7 +51,6 @@ import {
|
|||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
STATES_LIST,
|
STATES_LIST,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
import { ModuleIssuesGanttChartView } from "components/modules";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
type?: "issue" | "cycle" | "module";
|
||||||
@ -107,7 +101,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
groupByProperty: selectedGroup,
|
groupByProperty: selectedGroup,
|
||||||
orderBy,
|
orderBy,
|
||||||
filters,
|
filters,
|
||||||
isNotEmpty,
|
isEmpty,
|
||||||
setFilters,
|
setFilters,
|
||||||
params,
|
params,
|
||||||
} = useIssuesView();
|
} = useIssuesView();
|
||||||
@ -495,7 +489,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
{viewId ? "Update" : "Save"} view
|
{viewId ? "Update" : "Save"} view
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
{<div className="mt-3 border-t border-brand-base" />}
|
{<div className="mt-3 border-t border-custom-border-200" />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -505,7 +499,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||||
} fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-brand-base px-3 py-5 text-xs font-medium italic text-red-500 ${
|
} fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
|
||||||
snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
|
snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
|
||||||
} transition duration-300`}
|
} transition duration-300`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
@ -517,7 +511,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
{groupedByIssues ? (
|
{groupedByIssues ? (
|
||||||
isNotEmpty ? (
|
!isEmpty || issueView === "kanban" || issueView === "calendar" ? (
|
||||||
<>
|
<>
|
||||||
{isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
{isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
||||||
{issueView === "list" ? (
|
{issueView === "list" ? (
|
||||||
@ -584,46 +578,36 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
issueView === "gantt_chart" && <GanttChartView />
|
issueView === "gantt_chart" && <GanttChartView />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : type === "issue" ? (
|
) : router.pathname.includes("archived-issues") ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
type="issue"
|
title="Archived Issues will be shown here"
|
||||||
title="Create New Issue"
|
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
|
||||||
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
|
image={emptyIssueArchive}
|
||||||
imgURL={emptyIssue}
|
buttonText="Go to Automation Settings"
|
||||||
|
onClick={() => {
|
||||||
|
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
<EmptyState
|
||||||
<EmptySpace
|
title={
|
||||||
title="You don't have any issue yet."
|
cycleId
|
||||||
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
|
? "Cycle issues will appear here"
|
||||||
Icon={RectangleStackIcon}
|
: moduleId
|
||||||
>
|
? "Module issues will appear here"
|
||||||
<EmptySpaceItem
|
: "Project issues will appear here"
|
||||||
title="Create a new issue"
|
}
|
||||||
description={
|
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
|
||||||
<span>
|
image={emptyIssue}
|
||||||
Use <pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>{" "}
|
buttonText="New Issue"
|
||||||
shortcut to create a new issue
|
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||||
</span>
|
onClick={() => {
|
||||||
}
|
const e = new KeyboardEvent("keydown", {
|
||||||
Icon={PlusIcon}
|
key: "c",
|
||||||
action={() => {
|
});
|
||||||
const e = new KeyboardEvent("keydown", {
|
document.dispatchEvent(e);
|
||||||
key: "c",
|
}}
|
||||||
});
|
/>
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{openIssuesListModal && (
|
|
||||||
<EmptySpaceItem
|
|
||||||
title="Add an existing issue"
|
|
||||||
description="Open list"
|
|
||||||
Icon={ListBulletIcon}
|
|
||||||
action={openIssuesListModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</EmptySpace>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
@ -38,7 +38,7 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{groupedByIssues && (
|
{groupedByIssues && (
|
||||||
<div>
|
<div className="h-full overflow-y-auto">
|
||||||
{Object.keys(groupedByIssues).map((singleGroup) => {
|
{Object.keys(groupedByIssues).map((singleGroup) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
|
@ -84,6 +84,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -181,7 +182,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
const singleIssuePath = isArchivedIssues
|
||||||
|
? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`
|
||||||
|
: `/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`;
|
||||||
|
|
||||||
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted || isArchivedIssues;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -207,25 +212,21 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||||
Copy issue link
|
Copy issue link
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<a
|
<a href={singleIssuePath} target="_blank" rel="noreferrer noopener">
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
>
|
|
||||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||||
Open issue in new tab
|
Open issue in new tab
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
</a>
|
</a>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div
|
<div
|
||||||
className="flex flex-wrap items-center justify-between px-4 py-2.5 gap-2 border-b border-brand-base bg-brand-base last:border-b-0"
|
className="flex flex-wrap items-center justify-between px-4 py-2.5 gap-2 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu(true);
|
setContextMenu(true);
|
||||||
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
<Link href={singleIssuePath}>
|
||||||
<div className="flex-grow cursor-pointer">
|
<div className="flex-grow cursor-pointer">
|
||||||
<a className="group relative flex items-center gap-2">
|
<a className="group relative flex items-center gap-2">
|
||||||
{properties.key && (
|
{properties.key && (
|
||||||
@ -233,13 +234,13 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
tooltipHeading="Issue ID"
|
tooltipHeading="Issue ID"
|
||||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||||
>
|
>
|
||||||
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<span className="text-[0.825rem] text-brand-base">
|
<span className="text-[0.825rem] text-custom-text-100">
|
||||||
{truncateText(issue.name, 50)}
|
{truncateText(issue.name, 50)}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -247,7 +248,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex w-full flex-shrink flex-wrap items-center gap-2 text-xs sm:w-auto">
|
<div
|
||||||
|
className={`flex w-full flex-shrink flex-wrap items-center gap-2 text-xs sm:w-auto ${
|
||||||
|
isArchivedIssues ? "opacity-60" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{properties.priority && (
|
{properties.priority && (
|
||||||
<ViewPrioritySelect
|
<ViewPrioritySelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
@ -266,7 +271,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.due_date && (
|
{properties.due_date && issue.target_date && (
|
||||||
<ViewDueDateSelect
|
<ViewDueDateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
@ -274,7 +279,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.labels && (
|
{properties.labels && issue.labels.length > 0 && (
|
||||||
<ViewLabelSelect
|
<ViewLabelSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
@ -292,7 +297,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.estimate && (
|
{properties.estimate && issue.estimate_point !== null && (
|
||||||
<ViewEstimateSelect
|
<ViewEstimateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
@ -301,30 +306,30 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.sub_issue_count && (
|
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
|
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
||||||
{issue.sub_issues_count}
|
{issue.sub_issues_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.link && (
|
{properties.link && issue.link_count > 0 && (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<LinkIcon className="h-3.5 w-3.5" />
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
{issue.link_count}
|
{issue.link_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.attachment_count && (
|
{properties.attachment_count && issue.attachment_count > 0 && (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
||||||
{issue.attachment_count}
|
{issue.attachment_count}
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,7 +33,6 @@ import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
|||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
type?: "issue" | "cycle" | "module";
|
||||||
currentState?: IState | null;
|
currentState?: IState | null;
|
||||||
bgColor?: string;
|
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
groupedByIssues: {
|
groupedByIssues: {
|
||||||
[key: string]: IIssue[];
|
[key: string]: IIssue[];
|
||||||
@ -53,7 +52,6 @@ type Props = {
|
|||||||
export const SingleList: React.FC<Props> = ({
|
export const SingleList: React.FC<Props> = ({
|
||||||
type,
|
type,
|
||||||
currentState,
|
currentState,
|
||||||
bgColor,
|
|
||||||
groupTitle,
|
groupTitle,
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
@ -69,6 +67,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
@ -113,7 +112,8 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
|
|
||||||
switch (selectedGroup) {
|
switch (selectedGroup) {
|
||||||
case "state":
|
case "state":
|
||||||
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
|
icon =
|
||||||
|
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||||
break;
|
break;
|
||||||
case "priority":
|
case "priority":
|
||||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||||
@ -142,28 +142,30 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
<Disclosure as="div" defaultOpen>
|
<Disclosure as="div" defaultOpen>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between px-4 py-2.5">
|
<div className="flex items-center justify-between px-4 py-2.5 bg-custom-background-90">
|
||||||
<Disclosure.Button>
|
<Disclosure.Button>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
{selectedGroup !== null && (
|
{selectedGroup !== null && (
|
||||||
<div className="flex items-center">{getGroupIcon()}</div>
|
<div className="flex items-center">{getGroupIcon()}</div>
|
||||||
)}
|
)}
|
||||||
{selectedGroup !== null ? (
|
{selectedGroup !== null ? (
|
||||||
<h2 className="text-sm font-semibold capitalize leading-6 text-brand-base">
|
<h2 className="text-sm font-semibold capitalize leading-6 text-custom-text-100">
|
||||||
{getGroupTitle()}
|
{getGroupTitle()}
|
||||||
</h2>
|
</h2>
|
||||||
) : (
|
) : (
|
||||||
<h2 className="font-medium leading-5">All Issues</h2>
|
<h2 className="font-medium leading-5">All Issues</h2>
|
||||||
)}
|
)}
|
||||||
<span className="text-brand-2 min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs">
|
<span className="text-custom-text-200 min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs">
|
||||||
{groupedByIssues[groupTitle as keyof IIssue].length}
|
{groupedByIssues[groupTitle as keyof IIssue].length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
{type === "issue" ? (
|
{isArchivedIssues ? (
|
||||||
|
""
|
||||||
|
) : type === "issue" ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 text-brand-secondary hover:bg-brand-surface-2"
|
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
onClick={addIssueToState}
|
onClick={addIssueToState}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@ -222,7 +224,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="bg-brand-base px-4 py-2.5 text-sm text-brand-secondary">
|
<p className="bg-custom-background-100 px-4 py-2.5 text-sm text-custom-text-200">
|
||||||
No issues.
|
No issues.
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
|
@ -173,7 +173,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all">
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
|
||||||
<form>
|
<form>
|
||||||
<Combobox
|
<Combobox
|
||||||
onChange={(val: string) => {
|
onChange={(val: string) => {
|
||||||
@ -188,12 +188,12 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
>
|
>
|
||||||
<div className="relative m-1">
|
<div className="relative m-1">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -201,16 +201,16 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
|
|
||||||
<Combobox.Options
|
<Combobox.Options
|
||||||
static
|
static
|
||||||
className="max-h-80 scroll-py-2 divide-y divide-brand-base overflow-y-auto"
|
className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 overflow-y-auto"
|
||||||
>
|
>
|
||||||
{filteredIssues.length > 0 ? (
|
{filteredIssues.length > 0 ? (
|
||||||
<li className="p-2">
|
<li className="p-2">
|
||||||
{query === "" && (
|
{query === "" && (
|
||||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-brand-base">
|
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-custom-text-100">
|
||||||
Select issues to delete
|
Select issues to delete
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
<ul className="text-sm text-brand-secondary">
|
<ul className="text-sm text-custom-text-200">
|
||||||
{filteredIssues.map((issue) => (
|
{filteredIssues.map((issue) => (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
@ -218,7 +218,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
value={issue.id}
|
value={issue.id}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
||||||
active ? "bg-brand-surface-2 text-brand-base" : ""
|
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -246,9 +246,9 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||||
<LayerDiagonalIcon height="56" width="56" />
|
<LayerDiagonalIcon height="56" width="56" />
|
||||||
<h3 className="text-brand-secondary">
|
<h3 className="text-custom-text-200">
|
||||||
No issues found. Create a new issue with{" "}
|
No issues found. Create a new issue with{" "}
|
||||||
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>.
|
<pre className="inline rounded bg-custom-background-80 px-2 py-1">C</pre>.
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
@ -141,7 +141,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||||
@ -154,7 +154,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all">
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
as="div"
|
||||||
onChange={(val: ISearchIssueResponse) => {
|
onChange={(val: ISearchIssueResponse) => {
|
||||||
@ -165,24 +165,24 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<div className="relative m-1">
|
<div className="relative m-1">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||||
placeholder="Type to search..."
|
placeholder="Type to search..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-brand-secondary text-[0.825rem] p-2">
|
<div className="text-custom-text-200 text-[0.825rem] p-2">
|
||||||
{selectedIssues.length > 0 ? (
|
{selectedIssues.length > 0 ? (
|
||||||
<div className="flex items-center gap-2 flex-wrap mt-1">
|
<div className="flex items-center gap-2 flex-wrap mt-1">
|
||||||
{selectedIssues.map((issue) => (
|
{selectedIssues.map((issue) => (
|
||||||
<div
|
<div
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
className="flex items-center gap-1 text-xs border border-brand-base bg-brand-surface-2 pl-2 py-1 rounded-md text-brand-base whitespace-nowrap"
|
className="flex items-center gap-1 text-xs border border-custom-border-200 bg-custom-background-80 pl-2 py-1 rounded-md text-custom-text-100 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{issue.project__identifier}-{issue.sequence_id}
|
{issue.project__identifier}-{issue.sequence_id}
|
||||||
<button
|
<button
|
||||||
@ -194,13 +194,13 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<XMarkIcon className="h-3 w-3 text-brand-secondary group-hover:text-brand-base" />
|
<XMarkIcon className="h-3 w-3 text-custom-text-200 group-hover:text-custom-text-100" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-min text-xs border border-brand-base bg-brand-surface-2 p-2 rounded-md whitespace-nowrap">
|
<div className="w-min text-xs border border-custom-border-200 bg-custom-background-80 p-2 rounded-md whitespace-nowrap">
|
||||||
No issues selected
|
No issues selected
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -208,9 +208,9 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto mt-2">
|
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto mt-2">
|
||||||
{debouncedSearchTerm !== "" && (
|
{debouncedSearchTerm !== "" && (
|
||||||
<h5 className="text-[0.825rem] text-brand-secondary mx-2">
|
<h5 className="text-[0.825rem] text-custom-text-200 mx-2">
|
||||||
Search results for{" "}
|
Search results for{" "}
|
||||||
<span className="text-brand-base">
|
<span className="text-custom-text-100">
|
||||||
{'"'}
|
{'"'}
|
||||||
{debouncedSearchTerm}
|
{debouncedSearchTerm}
|
||||||
{'"'}
|
{'"'}
|
||||||
@ -225,9 +225,9 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
debouncedSearchTerm !== "" && (
|
debouncedSearchTerm !== "" && (
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||||
<LayerDiagonalIcon height="52" width="52" />
|
<LayerDiagonalIcon height="52" width="52" />
|
||||||
<h3 className="text-brand-secondary">
|
<h3 className="text-custom-text-200">
|
||||||
No issues found. Create a new issue with{" "}
|
No issues found. Create a new issue with{" "}
|
||||||
<pre className="inline rounded bg-brand-surface-2 px-2 py-1 text-sm">
|
<pre className="inline rounded bg-custom-background-80 px-2 py-1 text-sm">
|
||||||
C
|
C
|
||||||
</pre>
|
</pre>
|
||||||
.
|
.
|
||||||
@ -243,7 +243,9 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
) : (
|
) : (
|
||||||
<ul className={`text-sm text-brand-base ${issues.length > 0 ? "p-2" : ""}`}>
|
<ul
|
||||||
|
className={`text-sm text-custom-text-100 ${issues.length > 0 ? "p-2" : ""}`}
|
||||||
|
>
|
||||||
{issues.map((issue) => {
|
{issues.map((issue) => {
|
||||||
const selected = selectedIssues.some((i) => i.id === issue.id);
|
const selected = selectedIssues.some((i) => i.id === issue.id);
|
||||||
|
|
||||||
@ -254,9 +256,9 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
htmlFor={`issue-${issue.id}`}
|
htmlFor={`issue-${issue.id}`}
|
||||||
value={issue}
|
value={issue}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
|
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||||
active ? "bg-brand-surface-2 text-brand-base" : ""
|
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||||
} ${selected ? "text-brand-base" : ""}`
|
} ${selected ? "text-custom-text-100" : ""}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<input type="checkbox" checked={selected} readOnly />
|
<input type="checkbox" checked={selected} readOnly />
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user