Compare commits

..
Author SHA1 Message Date
sriram veeraghantaandGitHub 2bc05cc7b7 Merge pull request #2157 from makeplane/stage-release
release: self hosting fixes
2023-09-12 21:50:34 +05:30
sriram veeraghantaandGitHub 14fe545709 Merge pull request #2156 from makeplane/stage/self-hosted-fixes
fix: selfhosted fixes
2023-09-12 21:06:01 +05:30
sriram veeraghanta bc99ec0f1d fix: selfhosted fixes (#2154)
* fix: selfhosted fixes

* fix: updated env example
2023-09-12 21:03:42 +05:30
sriram veeraghantaandGitHub cc63f67654 Merge pull request #2143 from makeplane/stage-release
promote: stage-release to master
2023-09-11 18:21:51 +05:30
sriram veeraghantaandGitHub b8dd9ca729 Merge pull request #2144 from makeplane/develop
promote: develop to stage release
2023-09-11 18:11:30 +05:30
NikhilandGitHub 73b360c2fd Merge pull request #2140 from makeplane/develop
promote: develop to stage-release
2023-09-11 14:39:11 +05:30
sriram veeraghantaandGitHub 80bcca71ff Merge pull request #2091 from makeplane/stage-release
promote: stage release to master
2023-09-04 19:14:29 +05:30
sriram veeraghantaandGitHub 3db0ec819a Merge pull request #2090 from makeplane/develop
Promote: Develop to Stage Release
2023-09-04 18:53:41 +05:30
sriram veeraghantaandGitHub 414ea7371d Merge pull request #2085 from makeplane/develop
Promote: Develop to Stage Release
2023-09-04 18:16:47 +05:30
sriram veeraghantaandGitHub 2b84b7c18d Merge pull request #2076 from makeplane/develop
Promote: Develop to Stage Release
2023-09-04 14:48:53 +05:30
sriram veeraghantaandGitHub 674347c99e Merge pull request #2060 from makeplane/develop
Promote: Develop to Stage Release
2023-09-01 20:56:31 +05:30
sriram veeraghantaandGitHub c1102180e6 Merge pull request #2059 from makeplane/develop
Promote: Develop to Stage Release
2023-09-01 20:53:35 +05:30
sriram veeraghantaandGitHub 650c0c3b78 promote: develop to stage-release (#2045)
promote: develop to stage-release
2023-09-01 17:23:26 +05:30
sriram veeraghantaandGitHub 6f397710ce Merge pull request #1951 from makeplane/stage-release
Promote: Stage release to Production
2023-08-23 16:18:08 +05:30
sriram veeraghantaandGitHub e6bd6b6a8c Merge pull request #1950 from makeplane/develop
Promote: Develop to Stage Release
2023-08-23 15:49:14 +05:30
sriram veeraghantaandGitHub 9d3952006b Merge pull request #1948 from makeplane/stage-release
Promot: Stage Release to Production
2023-08-23 14:51:27 +05:30
sriram veeraghantaandGitHub b75473a684 Merge pull request #1947 from makeplane/develop
Promote: Develop to Stage Release
2023-08-23 12:37:25 +05:30
NikhilandGitHub 2f5bd58c61 Merge pull request #1941 from makeplane/stage-release
promote: stage-release to master
2023-08-22 19:37:49 +05:30
NikhilandGitHub e833fccf61 Merge pull request #1940 from makeplane/develop
dev: deploy docker containers (#1939)
2023-08-22 19:34:13 +05:30
NikhilandGitHub 62ba9abdb4 Merge pull request #1936 from makeplane/stage-release
promote: stage-release to master
2023-08-22 13:57:57 +05:30
NikhilandGitHub 46b138eb0b Merge pull request #1934 from makeplane/develop
fix: aws region changed for exporter (#1933)
2023-08-22 13:51:20 +05:30
NikhilandGitHub 59bdf222f5 Merge pull request #1932 from makeplane/stage-release
promote: stage release to master
2023-08-22 01:34:37 +05:30
NikhilandGitHub eb50ade5e3 Merge pull request #1931 from makeplane/develop
fix: access environment variables is changed in services (#1930)
2023-08-22 01:14:56 +05:30
Vamsi KuramaandGitHub 85a08e4abd Merge pull request #1929 from makeplane/stage-release 2023-08-22 00:17:29 +05:30
NikhilandGitHub aa2e1697b0 Merge pull request #1928 from makeplane/develop
promote: develop to stage-release
2023-08-21 20:51:46 +05:30
NikhilandGitHub 3beab9de6f Merge pull request #1923 from makeplane/develop
promote: develop to stage-release
2023-08-21 18:18:14 +05:30
NikhilandGitHub 13d21e752d Merge pull request #1910 from makeplane/develop
promote: develop to stage-release
2023-08-18 19:29:11 +05:30
Vamsi KuramaandGitHub 9ff8994c0e Merge pull request #1784 from makeplane/stage-release
promote: stage-release to master v0.10.1-patch
2023-08-03 18:55:24 +05:30
Vamsi KuramaandGitHub 3488001197 Merge pull request #1762 from makeplane/stage-release
promote: stage-release to master
2023-08-01 22:38:20 +05:30
Vamsi KuramaandGitHub 2ced7e4911 Merge pull request #1758 from makeplane/stage-release
promote: stage-release to master v0.10
2023-08-01 20:04:40 +05:30
Aaryan KhandelwalandGitHub e5a3bec28c Merge pull request #1709 from makeplane/stage-release
promote: stage-release to master
2023-07-28 19:47:04 +05:30
Vamsi KuramaandGitHub e930f8cc7b Merge pull request #1697 from makeplane/stage-release
promote: stage-release to master
2023-07-28 00:55:32 +05:30
233 changed files with 5690 additions and 9141 deletions
+54
View File
@@ -1,3 +1,38 @@
# Frontend
# Extra image domains that need to be added for Next Image
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
# Google Client ID for Google OAuth
NEXT_PUBLIC_GOOGLE_CLIENTID=""
# Github ID for Github OAuth
NEXT_PUBLIC_GITHUB_ID=""
# Github App Name for GitHub Integration
NEXT_PUBLIC_GITHUB_APP_NAME=""
# Sentry DSN for error monitoring
NEXT_PUBLIC_SENTRY_DSN=""
# Enable/Disable OAUTH - default 0 for selfhosted instance
NEXT_PUBLIC_ENABLE_OAUTH=0
# Enable/Disable sentry
NEXT_PUBLIC_ENABLE_SENTRY=0
# Enable/Disable session recording
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
# Enable/Disable event tracking
NEXT_PUBLIC_TRACK_EVENTS=0
# Slack for Slack Integration
NEXT_PUBLIC_SLACK_CLIENT_ID=""
# For Telemetry, set it to "app.plane.so"
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
# public boards deploy url
NEXT_PUBLIC_DEPLOY_URL=""
# plane deploy using nginx
NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
# Error logs
SENTRY_DSN=""
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
@@ -10,6 +45,15 @@ REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# Email Settings
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_USE_TLS="1"
EMAIL_USE_SSL="0"
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
@@ -25,6 +69,9 @@ OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
@@ -33,3 +80,10 @@ USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Default Creds
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
# Auto generated and Required that will be generated from setup.sh
-60
View File
@@ -1,60 +0,0 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
# Error logs
SENTRY_DSN=""
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
PGHOST="plane-db"
PGDATABASE="plane"
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
# Redis Settings
REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# Email Settings
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_USE_TLS="1"
EMAIL_USE_SSL="0"
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
AWS_SECRET_ACCESS_KEY="secret-key"
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
# Changing this requires change in the nginx.conf for uploads if using minio setup
AWS_S3_BUCKET_NAME="uploads"
# Maximum file upload limit
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Default Creds
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
+2 -2
View File
@@ -31,6 +31,8 @@ from .issue import (
IssueActivitySerializer,
IssueCommentSerializer,
IssuePropertySerializer,
BlockerIssueSerializer,
BlockedIssueSerializer,
IssueAssigneeSerializer,
LabelSerializer,
IssueSerializer,
@@ -43,8 +45,6 @@ from .issue import (
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
)
+106 -28
View File
@@ -17,10 +17,12 @@ from plane.db.models import (
IssueActivity,
IssueComment,
IssueProperty,
IssueBlocker,
IssueAssignee,
IssueSubscriber,
IssueLabel,
Label,
IssueBlocker,
CycleIssue,
Cycle,
Module,
@@ -30,7 +32,6 @@ from plane.db.models import (
IssueReaction,
CommentReaction,
IssueVote,
IssueRelation,
)
@@ -49,7 +50,6 @@ class IssueFlatSerializer(BaseSerializer):
"target_date",
"sequence_id",
"sort_order",
"is_draft",
]
@@ -81,12 +81,25 @@ class IssueCreateSerializer(BaseSerializer):
required=False,
)
# List of issues that are blocking this issue
blockers_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
write_only=True,
required=False,
)
labels_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
# List of issues that are blocked by this issue
blocks_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = Issue
fields = "__all__"
@@ -109,8 +122,10 @@ class IssueCreateSerializer(BaseSerializer):
return data
def create(self, validated_data):
blockers = validated_data.pop("blockers_list", None)
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
@@ -122,6 +137,22 @@ class IssueCreateSerializer(BaseSerializer):
created_by_id = issue.created_by_id
updated_by_id = issue.updated_by_id
if blockers is not None and len(blockers):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=issue,
blocked_by=blocker,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for blocker in blockers
],
batch_size=10,
)
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
[
@@ -165,11 +196,29 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if blocks is not None and len(blocks):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=block,
blocked_by=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for block in blocks
],
batch_size=10,
)
return issue
def update(self, instance, validated_data):
blockers = validated_data.pop("blockers_list", None)
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
# Related models
project_id = instance.project_id
@@ -177,6 +226,23 @@ class IssueCreateSerializer(BaseSerializer):
created_by_id = instance.created_by_id
updated_by_id = instance.updated_by_id
if blockers is not None:
IssueBlocker.objects.filter(block=instance).delete()
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=instance,
blocked_by=blocker,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for blocker in blockers
],
batch_size=10,
)
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete()
IssueAssignee.objects.bulk_create(
@@ -211,6 +277,23 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if blocks is not None:
IssueBlocker.objects.filter(blocked_by=instance).delete()
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=block,
blocked_by=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for block in blocks
],
batch_size=10,
)
# Time updation occues even when other related models are updated
instance.updated_at = timezone.now()
return super().update(instance, validated_data)
@@ -292,39 +375,32 @@ class IssueLabelSerializer(BaseSerializer):
]
class IssueRelationSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
class BlockedIssueSerializer(BaseSerializer):
blocked_issue_detail = IssueProjectLiteSerializer(source="block", read_only=True)
class Meta:
model = IssueRelation
model = IssueBlocker
fields = [
"issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
"project",
"blocked_issue_detail",
"blocked_by",
"block",
]
read_only_fields = fields
class RelatedIssueSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
class BlockerIssueSerializer(BaseSerializer):
blocker_issue_detail = IssueProjectLiteSerializer(
source="blocked_by", read_only=True
)
class Meta:
model = IssueRelation
model = IssueBlocker
fields = [
"issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
"project",
"blocker_issue_detail",
"blocked_by",
"block",
]
read_only_fields = fields
class IssueAssigneeSerializer(BaseSerializer):
@@ -541,8 +617,10 @@ class IssueSerializer(BaseSerializer):
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
label_details = LabelSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
# List of issues blocked by this issue
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
# List of issues that block this issue
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
issue_cycle = IssueCycleDetailSerializer(read_only=True)
issue_module = IssueModuleDetailSerializer(read_only=True)
issue_link = IssueLinkSerializer(read_only=True, many=True)
-45
View File
@@ -90,9 +90,7 @@ from plane.api.views import (
IssueSubscriberViewSet,
IssueCommentPublicViewSet,
IssueReactionViewSet,
IssueRelationViewSet,
CommentReactionViewSet,
IssueDraftViewSet,
## End Issues
# States
StateViewSet,
@@ -1012,49 +1010,6 @@ urlpatterns = [
name="project-issue-archive",
),
## End Issue Archives
## Issue Relation
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"post": "create",
}
),
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
IssueRelationViewSet.as_view(
{
"delete": "destroy",
}
),
name="issue-relation",
),
## End Issue Relation
## Issue Drafts
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
IssueDraftViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
IssueDraftViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-draft",
),
## End Issue Drafts
## File Assets
path(
"workspaces/<str:slug>/file-assets/",
+3 -3
View File
@@ -86,10 +86,8 @@ from .issue import (
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
IssueVotePublicViewSet,
IssueRelationViewSet,
IssueRetrievePublicEndpoint,
ProjectIssuesPublicEndpoint,
IssueDraftViewSet,
)
from .auth_extended import (
@@ -169,4 +167,6 @@ from .analytic import (
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
from .exporter import ExportIssuesEndpoint
from .exporter import (
ExportIssuesEndpoint,
)
+21 -116
View File
@@ -101,84 +101,48 @@ class CycleViewSet(BaseViewSet):
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(total_issues=Count("issue_cycle"))
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
filter=Q(issue_cycle__issue__state__group="cancelled"),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
filter=Q(issue_cycle__issue__state__group="unstarted"),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
filter=Q(issue_cycle__issue__state__group="backlog"),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.prefetch_related(
@@ -231,30 +195,17 @@ class CycleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar")
.annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=True),
)
)
.order_by("display_name")
@@ -270,30 +221,17 @@ class CycleViewSet(BaseViewSet):
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate(total_issues=Count("label_id"))
.annotate(
completed_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=True),
)
)
.order_by("label_name")
@@ -446,30 +384,17 @@ class CycleViewSet(BaseViewSet):
.values(
"first_name", "last_name", "assignee_id", "avatar", "display_name"
)
.annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=True),
)
)
.order_by("first_name", "last_name")
@@ -486,30 +411,17 @@ class CycleViewSet(BaseViewSet):
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(total_issues=Count("label_id"))
.annotate(
completed_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=True),
)
)
.order_by("label_name")
@@ -605,7 +517,6 @@ class CycleIssueViewSet(BaseViewSet):
try:
order_by = request.GET.get("order_by", "created_at")
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
@@ -644,15 +555,9 @@ class CycleIssueViewSet(BaseViewSet):
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues_data, group_by, sub_group_by),
group_results(issues_data, group_by),
status=status.HTTP_200_OK,
)
+7 -354
View File
@@ -24,7 +24,6 @@ from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.db import IntegrityError
from django.conf import settings
from django.db import IntegrityError
# Third Party imports
from rest_framework.response import Response
@@ -52,8 +51,6 @@ from plane.api.serializers import (
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
)
from plane.api.permissions import (
@@ -79,7 +76,6 @@ from plane.db.models import (
CommentReaction,
ProjectDeployBoard,
IssueVote,
IssueRelation,
ProjectPublicMember,
)
from plane.bgtasks.issue_activites_task import issue_activity
@@ -182,7 +178,7 @@ class IssueViewSet(BaseViewSet):
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
priority_order = ["urgent", "high", "medium", "low", None]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -270,16 +266,9 @@ class IssueViewSet(BaseViewSet):
## Grouping the results
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
group_results(issues, group_by), status=status.HTTP_200_OK
)
return Response(issues, status=status.HTTP_200_OK)
@@ -342,7 +331,7 @@ class UserWorkSpaceIssues(BaseAPIView):
try:
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
priority_order = ["urgent", "high", "medium", "low", None]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -454,16 +443,9 @@ class UserWorkSpaceIssues(BaseAPIView):
## Grouping the results
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
group_results(issues, group_by), status=status.HTTP_200_OK
)
return Response(issues, status=status.HTTP_200_OK)
@@ -509,7 +491,7 @@ class IssueActivityEndpoint(BaseAPIView):
issue_activities = (
IssueActivity.objects.filter(issue_id=issue_id)
.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
~Q(field__in=["comment", "vote", "reaction"]),
project__project_projectmember__member=self.request.user,
)
.select_related("actor", "workspace", "issue", "project")
@@ -1086,7 +1068,7 @@ class IssueArchiveViewSet(BaseViewSet):
show_sub_issues = request.GET.get("show_sub_issues", "true")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
priority_order = ["urgent", "high", "medium", "low", None]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -2058,105 +2040,6 @@ class IssueVotePublicViewSet(BaseViewSet):
)
class IssueRelationViewSet(BaseViewSet):
serializer_class = IssueRelationSerializer
model = IssueRelation
permission_classes = [
ProjectEntityPermission,
]
def perform_destroy(self, instance):
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="issue_relation.activity.deleted",
requested_data=json.dumps({"related_list": None}),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueRelationSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
return super().perform_destroy(instance)
def create(self, request, slug, project_id, issue_id):
try:
related_list = request.data.get("related_list", [])
relation = request.data.get("relation", None)
project = Project.objects.get(pk=project_id)
issue_relation = IssueRelation.objects.bulk_create(
[
IssueRelation(
issue_id=related_issue["issue"],
related_issue_id=related_issue["related_issue"],
relation_type=related_issue["relation_type"],
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for related_issue in related_list
],
batch_size=10,
ignore_conflicts=True,
)
issue_activity.delay(
type="issue_relation.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
)
if relation == "blocking":
return Response(
RelatedIssueSerializer(issue_relation, many=True).data,
status=status.HTTP_201_CREATED,
)
else:
return Response(
IssueRelationSerializer(issue_relation, many=True).data,
status=status.HTTP_201_CREATED,
)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The issue is already taken"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
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 get_queryset(self):
return self.filter_queryset(
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)
.select_related("project")
.select_related("workspace")
.select_related("issue")
.distinct()
)
class IssueRetrievePublicEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
@@ -2195,7 +2078,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
priority_order = ["urgent", "high", "medium", "low", None]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -2357,233 +2240,3 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueDraftViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = IssueFlatSerializer
model = Issue
def perform_update(self, serializer):
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="issue_draft.activity.updated",
requested_data=requested_data,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
)
return super().perform_update(serializer)
def perform_destroy(self, instance):
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="issue_draft.activity.deleted",
requested_data=json.dumps(
{"issue_id": str(self.kwargs.get("pk", None))}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
)
return super().perform_destroy(instance)
def get_queryset(self):
return (
Issue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(is_draft=True)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
)
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
try:
filters = issue_filters(request.query_params, "GET")
# 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)
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 create(self, request, slug, project_id):
try:
project = Project.objects.get(pk=project_id)
serializer = IssueCreateSerializer(
data=request.data,
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
"default_assignee_id": project.default_assignee_id,
},
)
if serializer.is_valid():
serializer.save(is_draft=True)
# Track the issue
issue_activity.delay(
type="issue_draft.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
current_instance=None,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Project.DoesNotExist:
return Response(
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
)
def retrieve(self, request, slug, project_id, pk=None):
try:
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True
)
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
)
+15 -84
View File
@@ -39,7 +39,6 @@ from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
from plane.utils.analytics_plot import burndown_plot
class ModuleViewSet(BaseViewSet):
model = Module
permission_classes = [
@@ -78,63 +77,35 @@ class ModuleViewSet(BaseViewSet):
queryset=ModuleLink.objects.select_related("module", "created_by"),
)
)
.annotate(
total_issues=Count(
"issue_module",
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
),
)
.annotate(total_issues=Count("issue_module"))
.annotate(
completed_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
filter=Q(issue_module__issue__state__group="completed"),
)
)
.annotate(
cancelled_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
filter=Q(issue_module__issue__state__group="cancelled"),
)
)
.annotate(
started_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
filter=Q(issue_module__issue__state__group="started"),
)
)
.annotate(
unstarted_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
filter=Q(issue_module__issue__state__group="unstarted"),
)
)
.annotate(
backlog_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
filter=Q(issue_module__issue__state__group="backlog"),
)
)
.order_by(order_by, "name")
@@ -206,36 +177,18 @@ class ModuleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id"))
.annotate(display_name=F("assignees__display_name"))
.annotate(avatar=F("assignees__avatar"))
.values(
"first_name", "last_name", "assignee_id", "avatar", "display_name"
)
.annotate(
total_issues=Count(
"assignee_id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
)
)
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=True),
)
)
.order_by("first_name", "last_name")
@@ -251,33 +204,17 @@ class ModuleViewSet(BaseViewSet):
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(total_issues=Count("label_id"))
.annotate(
completed_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
filter=Q(completed_at__isnull=True),
)
)
.order_by("label_name")
@@ -371,7 +308,6 @@ class ModuleIssueViewSet(BaseViewSet):
try:
order_by = request.GET.get("order_by", "created_at")
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.issue_objects.filter(issue_module__module_id=module_id)
@@ -410,15 +346,9 @@ class ModuleIssueViewSet(BaseViewSet):
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues_data, group_by, sub_group_by),
group_results(issues_data, group_by),
status=status.HTTP_200_OK,
)
@@ -553,6 +483,7 @@ class ModuleLinkViewSet(BaseViewSet):
class ModuleFavoriteViewSet(BaseViewSet):
serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite
+4 -4
View File
@@ -220,7 +220,7 @@ class IssueSearchEndpoint(BaseAPIView):
query = request.query_params.get("search", False)
workspace_search = request.query_params.get("workspace_search", "false")
parent = request.query_params.get("parent", "false")
issue_relation = request.query_params.get("issue_relation", "false")
blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
cycle = request.query_params.get("cycle", "false")
module = request.query_params.get("module", "false")
sub_issue = request.query_params.get("sub_issue", "false")
@@ -247,12 +247,12 @@ class IssueSearchEndpoint(BaseAPIView):
"parent_id", flat=True
)
)
if issue_relation == "true" and issue_id:
if blocker_blocked_by == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id),
~Q(issue_related__issue=issue),
~Q(issue_relation__related_issue=issue),
~Q(blocked_issues__block=issue),
~Q(blocker_issues__blocked_by=issue),
)
if sub_issue == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
+3 -15
View File
@@ -1072,7 +1072,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
.order_by("state_group")
)
priority_order = ["urgent", "high", "medium", "low", "none"]
priority_order = ["urgent", "high", "medium", "low", None]
priority_distribution = (
Issue.issue_objects.filter(
@@ -1239,21 +1239,13 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
.annotate(
created_issues=Count(
"project_issue",
filter=Q(
project_issue__created_by_id=user_id,
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
filter=Q(project_issue__created_by_id=user_id),
)
)
.annotate(
assigned_issues=Count(
"project_issue",
filter=Q(
project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
filter=Q(project_issue__assignees__in=[user_id]),
)
)
.annotate(
@@ -1262,8 +1254,6 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
filter=Q(
project_issue__completed_at__isnull=False,
project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
)
)
@@ -1277,8 +1267,6 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
"started",
],
project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
)
)
+140 -166
View File
@@ -393,19 +393,143 @@ def track_assignees(
)
# Track changes in blocking issues
def track_blocks(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
):
if len(requested_data.get("blocks_list")) > len(
current_instance.get("blocked_issues")
):
for block in requested_data.get("blocks_list"):
if (
len(
[
blocked
for blocked in current_instance.get("blocked_issues")
if blocked.get("block") == block
]
)
== 0
):
issue = Issue.objects.get(pk=block)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value="",
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
field="blocks",
project=project,
workspace=project.workspace,
comment=f"added blocking issue {project.identifier}-{issue.sequence_id}",
new_identifier=issue.id,
)
)
# Blocked Issue Removal
if len(requested_data.get("blocks_list")) < len(
current_instance.get("blocked_issues")
):
for blocked in current_instance.get("blocked_issues"):
if blocked.get("block") not in requested_data.get("blocks_list"):
issue = Issue.objects.get(pk=blocked.get("block"))
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field="blocks",
project=project,
workspace=project.workspace,
comment=f"removed blocking issue {project.identifier}-{issue.sequence_id}",
old_identifier=issue.id,
)
)
# Track changes in blocked_by issues
def track_blockings(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
):
if len(requested_data.get("blockers_list")) > len(
current_instance.get("blocker_issues")
):
for block in requested_data.get("blockers_list"):
if (
len(
[
blocked
for blocked in current_instance.get("blocker_issues")
if blocked.get("blocked_by") == block
]
)
== 0
):
issue = Issue.objects.get(pk=block)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value="",
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
field="blocking",
project=project,
workspace=project.workspace,
comment=f"added blocked by issue {project.identifier}-{issue.sequence_id}",
new_identifier=issue.id,
)
)
# Blocked Issue Removal
if len(requested_data.get("blockers_list")) < len(
current_instance.get("blocker_issues")
):
for blocked in current_instance.get("blocker_issues"):
if blocked.get("blocked_by") not in requested_data.get("blockers_list"):
issue = Issue.objects.get(pk=blocked.get("blocked_by"))
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field="blocking",
project=project,
workspace=project.workspace,
comment=f"removed blocked by issue {project.identifier}-{issue.sequence_id}",
old_identifier=issue.id,
)
)
def create_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"created the issue",
verb="created",
actor=actor,
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"created the issue",
verb="created",
actor=actor,
)
)
def track_estimate_points(
@@ -513,11 +637,18 @@ def update_issue_activity(
"start_date": track_start_date,
"labels_list": track_labels,
"assignees_list": track_assignees,
"blocks_list": track_blocks,
"blockers_list": track_blockings,
"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
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
if func is not None:
@@ -1039,158 +1170,6 @@ def delete_issue_vote_activity(
)
def create_issue_relation_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance is None and requested_data.get("related_list") is not None:
for issue_relation in requested_data.get("related_list"):
if issue_relation.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = issue_relation.get("relation_type")
issue = Issue.objects.get(pk=issue_relation.get("issue"))
issue_activities.append(
IssueActivity(
issue_id=issue_relation.get("related_issue"),
actor=actor,
verb="created",
old_value="",
new_value=f"{project.identifier}-{issue.sequence_id}",
field=relation_type,
project=project,
workspace=project.workspace,
comment=f'added {relation_type} relation',
old_identifier=issue_relation.get("issue"),
)
)
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
issue_activities.append(
IssueActivity(
issue_id=issue_relation.get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=f"{project.identifier}-{issue.sequence_id}",
field=f'{issue_relation.get("relation_type")}',
project=project,
workspace=project.workspace,
comment=f'added {issue_relation.get("relation_type")} relation',
old_identifier=issue_relation.get("related_issue"),
)
)
def delete_issue_relation_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance is not None and requested_data.get("related_list") is None:
if current_instance.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = current_instance.get("relation_type")
issue = Issue.objects.get(pk=current_instance.get("issue"))
issue_activities.append(
IssueActivity(
issue_id=current_instance.get("related_issue"),
actor=actor,
verb="deleted",
old_value=f"{project.identifier}-{issue.sequence_id}",
new_value="",
field=relation_type,
project=project,
workspace=project.workspace,
comment=f'deleted {relation_type} relation',
old_identifier=current_instance.get("issue"),
)
)
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
issue_activities.append(
IssueActivity(
issue_id=current_instance.get("issue"),
actor=actor,
verb="deleted",
old_value=f"{project.identifier}-{issue.sequence_id}",
new_value="",
field=f'{current_instance.get("relation_type")}',
project=project,
workspace=project.workspace,
comment=f'deleted {current_instance.get("relation_type")} relation',
old_identifier=current_instance.get("related_issue"),
)
)
def create_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"drafted the issue",
field="draft",
verb="created",
actor=actor,
)
)
def update_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if requested_data.get("is_draft") is not None and requested_data.get("is_draft") == False:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"created the issue",
verb="updated",
actor=actor,
)
)
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"updated the draft issue",
field="draft",
verb="updated",
actor=actor,
)
)
def delete_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
project=project,
workspace=project.workspace,
comment=f"deleted the draft issue",
field="draft",
verb="deleted",
actor=actor,
)
)
# Receive message from room group
@shared_task
def issue_activity(
@@ -1254,17 +1233,12 @@ def issue_activity(
"link.activity.deleted": delete_link_activity,
"attachment.activity.created": create_attachment_activity,
"attachment.activity.deleted": delete_attachment_activity,
"issue_relation.activity.created": create_issue_relation_activity,
"issue_relation.activity.deleted": delete_issue_relation_activity,
"issue_reaction.activity.created": create_issue_reaction_activity,
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
"comment_reaction.activity.created": create_comment_reaction_activity,
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
"issue_vote.activity.created": create_issue_vote_activity,
"issue_vote.activity.deleted": delete_issue_vote_activity,
"issue_draft.activity.created": create_draft_issue_activity,
"issue_draft.activity.updated": update_draft_issue_activity,
"issue_draft.activity.deleted": delete_draft_issue_activity,
}
func = ACTIVITY_MAPPER.get(type)
@@ -1,84 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-12 07:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from plane.db.models import IssueRelation
from sentry_sdk import capture_exception
import uuid
def create_issue_relation(apps, schema_editor):
try:
IssueBlockerModel = apps.get_model("db", "IssueBlocker")
updated_issue_relation = []
for blocked_issue in IssueBlockerModel.objects.all():
updated_issue_relation.append(
IssueRelation(
issue_id=blocked_issue.block_id,
related_issue_id=blocked_issue.blocked_by_id,
relation_type="blocked_by",
project_id=blocked_issue.project_id,
workspace_id=blocked_issue.workspace_id,
created_by_id=blocked_issue.created_by_id,
updated_by_id=blocked_issue.updated_by_id,
)
)
IssueRelation.objects.bulk_create(updated_issue_relation, batch_size=100)
except Exception as e:
print(e)
capture_exception(e)
def update_issue_priority_choice(apps, schema_editor):
IssueModel = apps.get_model("db", "Issue")
updated_issues = []
for obj in IssueModel.objects.all():
if obj.priority is None:
obj.priority = "none"
updated_issues.append(obj)
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0042_alter_analyticview_created_by_and_more'),
]
operations = [
migrations.CreateModel(
name='IssueRelation',
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)),
('relation_type', models.CharField(choices=[('duplicate', 'Duplicate'), ('relates_to', 'Relates To'), ('blocked_by', 'Blocked By')], default='blocked_by', max_length=20, verbose_name='Issue Relation Type')),
('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_relation', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('related_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_related', to='db.issue')),
('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 Relation',
'verbose_name_plural': 'Issue Relations',
'db_table': 'issue_relations',
'ordering': ('-created_at',),
'unique_together': {('issue', 'related_issue')},
},
),
migrations.AddField(
model_name='issue',
name='is_draft',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='issue',
name='priority',
field=models.CharField(choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low'), ('none', 'None')], default='none', max_length=30, verbose_name='Issue Priority'),
),
migrations.RunPython(create_issue_relation),
migrations.RunPython(update_issue_priority_choice),
]
@@ -1,138 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-13 07:09
from django.db import migrations
def workspace_member_props(old_props):
new_props = {
"filters": {
"priority": old_props.get("filters", {}).get("priority", None),
"state": old_props.get("filters", {}).get("state", None),
"state_group": old_props.get("filters", {}).get("state_group", None),
"assignees": old_props.get("filters", {}).get("assignees", None),
"created_by": old_props.get("filters", {}).get("created_by", None),
"labels": old_props.get("filters", {}).get("labels", None),
"start_date": old_props.get("filters", {}).get("start_date", None),
"target_date": old_props.get("filters", {}).get("target_date", None),
"subscriber": old_props.get("filters", {}).get("subscriber", None),
},
"display_filters": {
"group_by": old_props.get("groupByProperty", None),
"order_by": old_props.get("orderBy", "-created_at"),
"type": old_props.get("filters", {}).get("type", None),
"sub_issue": old_props.get("showSubIssues", True),
"show_empty_groups": old_props.get("showEmptyGroups", True),
"layout": old_props.get("issueView", "list"),
"calendar_date_range": old_props.get("calendarDateRange", ""),
},
"display_properties": {
"assignee": old_props.get("properties", {}).get("assignee",None),
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
"created_on": old_props.get("properties", {}).get("created_on", None),
"due_date": old_props.get("properties", {}).get("due_date", None),
"estimate": old_props.get("properties", {}).get("estimate", None),
"key": old_props.get("properties", {}).get("key", None),
"labels": old_props.get("properties", {}).get("labels", None),
"link": old_props.get("properties", {}).get("link", None),
"priority": old_props.get("properties", {}).get("priority", None),
"start_date": old_props.get("properties", {}).get("start_date", None),
"state": old_props.get("properties", {}).get("state", None),
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
"updated_on": old_props.get("properties", {}).get("updated_on", None),
},
}
return new_props
def project_member_props(old_props):
new_props = {
"filters": {
"priority": old_props.get("filters", {}).get("priority", None),
"state": old_props.get("filters", {}).get("state", None),
"state_group": old_props.get("filters", {}).get("state_group", None),
"assignees": old_props.get("filters", {}).get("assignees", None),
"created_by": old_props.get("filters", {}).get("created_by", None),
"labels": old_props.get("filters", {}).get("labels", None),
"start_date": old_props.get("filters", {}).get("start_date", None),
"target_date": old_props.get("filters", {}).get("target_date", None),
"subscriber": old_props.get("filters", {}).get("subscriber", None),
},
"display_filters": {
"group_by": old_props.get("groupByProperty", None),
"order_by": old_props.get("orderBy", "-created_at"),
"type": old_props.get("filters", {}).get("type", None),
"sub_issue": old_props.get("showSubIssues", True),
"show_empty_groups": old_props.get("showEmptyGroups", True),
"layout": old_props.get("issueView", "list"),
"calendar_date_range": old_props.get("calendarDateRange", ""),
},
}
return new_props
def cycle_module_props(old_props):
new_props = {
"filters": {
"priority": old_props.get("filters", {}).get("priority", None),
"state": old_props.get("filters", {}).get("state", None),
"state_group": old_props.get("filters", {}).get("state_group", None),
"assignees": old_props.get("filters", {}).get("assignees", None),
"created_by": old_props.get("filters", {}).get("created_by", None),
"labels": old_props.get("filters", {}).get("labels", None),
"start_date": old_props.get("filters", {}).get("start_date", None),
"target_date": old_props.get("filters", {}).get("target_date", None),
"subscriber": old_props.get("filters", {}).get("subscriber", None),
},
}
return new_props
def update_workspace_member_view_props(apps, schema_editor):
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
updated_workspace_member = []
for obj in WorkspaceMemberModel.objects.all():
obj.view_props = workspace_member_props(obj.view_props)
obj.default_props = workspace_member_props(obj.default_props)
updated_workspace_member.append(obj)
WorkspaceMemberModel.objects.bulk_update(updated_workspace_member, ["view_props", "default_props"], batch_size=100)
def update_project_member_view_props(apps, schema_editor):
ProjectMemberModel = apps.get_model("db", "ProjectMember")
updated_project_member = []
for obj in ProjectMemberModel.objects.all():
obj.view_props = project_member_props(obj.view_props)
obj.default_props = project_member_props(obj.default_props)
updated_project_member.append(obj)
ProjectMemberModel.objects.bulk_update(updated_project_member, ["view_props", "default_props"], batch_size=100)
def update_cycle_props(apps, schema_editor):
CycleModel = apps.get_model("db", "Cycle")
updated_cycle = []
for obj in CycleModel.objects.all():
if "filter" in obj.view_props:
obj.view_props = cycle_module_props(obj.view_props)
updated_cycle.append(obj)
CycleModel.objects.bulk_update(updated_cycle, ["view_props"], batch_size=100)
def update_module_props(apps, schema_editor):
ModuleModel = apps.get_model("db", "Module")
updated_module = []
for obj in ModuleModel.objects.all():
if "filter" in obj.view_props:
obj.view_props = cycle_module_props(obj.view_props)
updated_module.append(obj)
ModuleModel.objects.bulk_update(updated_module, ["view_props"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0043_alter_analyticview_created_by_and_more'),
]
operations = [
migrations.RunPython(update_workspace_member_view_props),
migrations.RunPython(update_project_member_view_props),
migrations.RunPython(update_cycle_props),
migrations.RunPython(update_module_props),
]
@@ -1,23 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-15 06:55
from django.db import migrations
def update_issue_activity(apps, schema_editor):
IssueActivityModel = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivityModel.objects.all():
if obj.field == "blocks":
obj.field = "blocked_by"
updated_issue_activity.append(obj)
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0044_auto_20230913_0709'),
]
operations = [
migrations.RunPython(update_issue_activity),
]
-1
View File
@@ -32,7 +32,6 @@ from .issue import (
IssueAssignee,
Label,
IssueBlocker,
IssueRelation,
IssueLink,
IssueSequence,
IssueAttachment,
+2 -35
View File
@@ -29,7 +29,6 @@ class IssueManager(models.Manager):
| models.Q(issue_inbox__isnull=True)
)
.exclude(archived_at__isnull=False)
.exclude(is_draft=True)
)
@@ -39,7 +38,6 @@ class Issue(ProjectBaseModel):
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("none", "None")
)
parent = models.ForeignKey(
"self",
@@ -66,7 +64,8 @@ class Issue(ProjectBaseModel):
max_length=30,
choices=PRIORITY_CHOICES,
verbose_name="Issue Priority",
default="none",
null=True,
blank=True,
)
start_date = models.DateField(null=True, blank=True)
target_date = models.DateField(null=True, blank=True)
@@ -84,7 +83,6 @@ class Issue(ProjectBaseModel):
sort_order = models.FloatField(default=65535)
completed_at = models.DateTimeField(null=True)
archived_at = models.DateField(null=True)
is_draft = models.BooleanField(default=False)
objects = models.Manager()
issue_objects = IssueManager()
@@ -180,37 +178,6 @@ class IssueBlocker(ProjectBaseModel):
return f"{self.block.name} {self.blocked_by.name}"
class IssueRelation(ProjectBaseModel):
RELATION_CHOICES = (
("duplicate", "Duplicate"),
("relates_to", "Relates To"),
("blocked_by", "Blocked By"),
)
issue = models.ForeignKey(
Issue, related_name="issue_relation", on_delete=models.CASCADE
)
related_issue = models.ForeignKey(
Issue, related_name="issue_related", on_delete=models.CASCADE
)
relation_type = models.CharField(
max_length=20,
choices=RELATION_CHOICES,
verbose_name="Issue Relation Type",
default="blocked_by",
)
class Meta:
unique_together = ["issue", "related_issue"]
verbose_name = "Issue Relation"
verbose_name_plural = "Issue Relations"
db_table = "issue_relations"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.related_issue.name}"
class IssueAssignee(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
+7 -20
View File
@@ -25,26 +25,13 @@ ROLE_CHOICES = (
def get_default_props():
return {
"filters": {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
},
"display_filters": {
"group_by": None,
"order_by": '-created_at',
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
},
"filters": {"type": None},
"orderBy": "-created_at",
"collapsed": True,
"issueView": "list",
"filterIssue": None,
"groupByProperty": None,
"showEmptyGroups": True,
}
+12 -27
View File
@@ -16,41 +16,26 @@ ROLE_CHOICES = (
def get_default_props():
return {
"filters": {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
},
"display_filters": {
"group_by": None,
"order_by": '-created_at',
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
},
"display_properties": {
"filters": {"type": None},
"groupByProperty": None,
"issueView": "list",
"orderBy": "-created_at",
"properties": {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"attachment_count": True,
"link": True,
"estimate": True,
"created_on": True,
"updated_on": True,
}
"start_date": True,
},
"showEmptyGroups": True,
}
+29 -131
View File
@@ -15,7 +15,7 @@ def resolve_keys(group_keys, value):
return value
def group_results(results_data, group_by, sub_group_by=False):
def group_results(results_data, group_by):
"""group results data into certain group_by
Args:
@@ -25,140 +25,38 @@ def group_results(results_data, group_by, sub_group_by=False):
Returns:
obj: grouped results
"""
if sub_group_by:
main_responsive_dict = dict()
response_dict = dict()
if sub_group_by == "priority":
main_responsive_dict = {
"urgent": {},
"high": {},
"medium": {},
"low": {},
"none": {},
}
if group_by == "priority":
response_dict = {
"urgent": [],
"high": [],
"medium": [],
"low": [],
"None": [],
}
for value in results_data:
main_group_attribute = resolve_keys(sub_group_by, value)
group_attribute = resolve_keys(group_by, value)
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
if len(main_group_attribute):
for attrib in main_group_attribute:
if str(attrib) not in main_responsive_dict:
main_responsive_dict[str(attrib)] = {}
if str(group_attribute) in main_responsive_dict[str(attrib)]:
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
else:
main_responsive_dict[str(attrib)][str(group_attribute)] = []
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
else:
if str(None) not in main_responsive_dict:
main_responsive_dict[str(None)] = {}
if str(group_attribute) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(group_attribute)].append(value)
for value in results_data:
group_attribute = resolve_keys(group_by, value)
if isinstance(group_attribute, list):
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in response_dict:
response_dict[str(attrib)].append(value)
else:
main_responsive_dict[str(None)][str(group_attribute)] = []
main_responsive_dict[str(None)][str(group_attribute)].append(value)
elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list):
if str(main_group_attribute) not in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(attrib)] = []
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(None)] = []
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list):
if len(main_group_attribute):
for main_attrib in main_group_attribute:
if str(main_attrib) not in main_responsive_dict:
main_responsive_dict[str(main_attrib)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(main_attrib)]:
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
else:
main_responsive_dict[str(main_attrib)][str(attrib)] = []
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(main_attrib)]:
main_responsive_dict[str(main_attrib)][str(None)].append(value)
else:
main_responsive_dict[str(main_attrib)][str(None)] = []
main_responsive_dict[str(main_attrib)][str(None)].append(value)
else:
if str(None) not in main_responsive_dict:
main_responsive_dict[str(None)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(attrib)].append(value)
else:
main_responsive_dict[str(None)][str(attrib)] = []
main_responsive_dict[str(None)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(None)].append(value)
else:
main_responsive_dict[str(None)][str(None)] = []
main_responsive_dict[str(None)][str(None)].append(value)
response_dict[str(attrib)] = []
response_dict[str(attrib)].append(value)
else:
main_group_attribute = resolve_keys(sub_group_by, value)
group_attribute = resolve_keys(group_by, value)
if str(main_group_attribute) not in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)] = {}
if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
if str(None) in response_dict:
response_dict[str(None)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
return main_responsive_dict
else:
response_dict = dict()
if group_by == "priority":
response_dict = {
"urgent": [],
"high": [],
"medium": [],
"low": [],
"none": [],
}
for value in results_data:
group_attribute = resolve_keys(group_by, value)
if isinstance(group_attribute, list):
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in response_dict:
response_dict[str(attrib)].append(value)
else:
response_dict[str(attrib)] = []
response_dict[str(attrib)].append(value)
else:
if str(None) in response_dict:
response_dict[str(None)].append(value)
else:
response_dict[str(None)] = []
response_dict[str(None)].append(value)
response_dict[str(None)] = []
response_dict[str(None)].append(value)
else:
if str(group_attribute) in response_dict:
response_dict[str(group_attribute)].append(value)
else:
if str(group_attribute) in response_dict:
response_dict[str(group_attribute)].append(value)
else:
response_dict[str(group_attribute)] = []
response_dict[str(group_attribute)].append(value)
response_dict[str(group_attribute)] = []
response_dict[str(group_attribute)].append(value)
return response_dict
return response_dict
+20 -4
View File
@@ -1,7 +1,6 @@
from django.utils.timezone import make_aware
from django.utils.dateparse import parse_datetime
def filter_state(params, filter, method):
if method == "GET":
states = params.get("state").split(",")
@@ -24,6 +23,7 @@ def filter_state_group(params, filter, method):
return filter
def filter_estimate_point(params, filter, method):
if method == "GET":
estimate_points = params.get("estimate_point").split(",")
@@ -39,10 +39,25 @@ def filter_priority(params, filter, method):
if method == "GET":
priorities = params.get("priority").split(",")
if len(priorities) and "" not in priorities:
filter["priority__in"] = priorities
if len(priorities) == 1 and "null" in priorities:
filter["priority__isnull"] = True
elif len(priorities) > 1 and "null" in priorities:
filter["priority__isnull"] = True
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
if params.get("priority", None) and len(params.get("priority")):
filter["priority__in"] = params.get("priority")
priorities = params.get("priority")
if len(priorities) == 1 and "null" in priorities:
filter["priority__isnull"] = True
elif len(priorities) > 1 and "null" in priorities:
filter["priority__isnull"] = True
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
filter["priority__in"] = [p for p in priorities if p != "null"]
return filter
@@ -214,6 +229,7 @@ def filter_issue_state_type(params, filter, method):
return filter
def filter_project(params, filter, method):
if method == "GET":
projects = params.get("project").split(",")
@@ -313,7 +329,7 @@ def issue_filters(query_params, method):
"module": filter_module,
"inbox_status": filter_inbox_status,
"sub_issue": filter_sub_issue_toggle,
"subscriber": filter_subscribed_issues,
"subscriber": filter_subscribed_issues,
"start_target_date": filter_start_target_date_issues,
}
+66 -3
View File
@@ -1,5 +1,37 @@
version: "3.8"
x-api-and-worker-env: &api-and-worker-env
DEBUG: ${DEBUG}
SENTRY_DSN: ${SENTRY_DSN}
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
REDIS_URL: redis://plane-redis:6379/
EMAIL_HOST: ${EMAIL_HOST}
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
EMAIL_PORT: ${EMAIL_PORT}
EMAIL_FROM: ${EMAIL_FROM}
EMAIL_USE_TLS: ${EMAIL_USE_TLS}
EMAIL_USE_SSL: ${EMAIL_USE_SSL}
AWS_REGION: ${AWS_REGION}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL}
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT}
WEB_URL: ${WEB_URL}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
DISABLE_COLLECTSTATIC: 1
DOCKERIZED: 1
OPENAI_API_BASE: ${OPENAI_API_BASE}
OPENAI_API_KEY: ${OPENAI_API_KEY}
GPT_ENGINE: ${GPT_ENGINE}
SECRET_KEY: ${SECRET_KEY}
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
USE_MINIO: ${USE_MINIO}
ENABLE_SIGNUP: ${ENABLE_SIGNUP}
services:
plane-web:
container_name: planefrontend
@@ -8,8 +40,23 @@ services:
dockerfile: ./web/Dockerfile.web
args:
DOCKER_BUILDKIT: 1
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces
restart: always
command: /usr/local/bin/start.sh web/server.js web
env_file:
- .env
environment:
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL}
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
NEXT_PUBLIC_GITHUB_ID: "0"
NEXT_PUBLIC_SENTRY_DSN: "0"
NEXT_PUBLIC_ENABLE_OAUTH: "0"
NEXT_PUBLIC_ENABLE_SENTRY: "0"
NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0"
NEXT_PUBLIC_TRACK_EVENTS: "0"
depends_on:
- plane-api
- plane-worker
@@ -21,8 +68,14 @@ services:
dockerfile: ./space/Dockerfile.space
args:
DOCKER_BUILDKIT: 1
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
restart: always
command: /usr/local/bin/start.sh space/server.js space
env_file:
- .env
environment:
- NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
depends_on:
- plane-api
- plane-worker
@@ -38,7 +91,9 @@ services:
restart: always
command: ./bin/takeoff
env_file:
- ./apiserver/.env
- .env
environment:
<<: *api-and-worker-env
depends_on:
- plane-db
- plane-redis
@@ -53,7 +108,9 @@ services:
restart: always
command: ./bin/worker
env_file:
- ./apiserver/.env
- .env
environment:
<<: *api-and-worker-env
depends_on:
- plane-api
- plane-db
@@ -69,7 +126,9 @@ services:
restart: always
command: ./bin/beat
env_file:
- ./apiserver/.env
- .env
environment:
<<: *api-and-worker-env
depends_on:
- plane-api
- plane-db
@@ -104,6 +163,8 @@ services:
command: server /export --console-address ":9090"
volumes:
- uploads:/export
env_file:
- .env
environment:
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
@@ -126,6 +187,8 @@ services:
restart: always
ports:
- ${NGINX_PORT}:80
env_file:
- .env
environment:
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
+22 -21
View File
@@ -1,29 +1,30 @@
events { }
http {
sendfile on;
sendfile on;
server {
listen 80;
root /www/data/;
access_log /var/log/nginx/access.log;
server {
listen 80;
root /www/data/;
access_log /var/log/nginx/access.log;
client_max_body_size ${FILE_SIZE_LIMIT};
client_max_body_size ${FILE_SIZE_LIMIT};
location / {
proxy_pass http://planefrontend:3000/;
}
location /api/ {
proxy_pass http://planebackend:8000/api/;
}
location /spaces/ {
proxy_pass http://planedeploy:3000/spaces/;
}
location /${BUCKET_NAME}/ {
proxy_pass http://plane-minio:9000/uploads/;
}
location / {
proxy_pass http://planefrontend:3000/;
}
location /api/ {
proxy_pass http://planebackend:8000/api/;
}
location /spaces/ {
proxy_pass http://planedeploy:3000/spaces/;
}
location /${BUCKET_NAME}/ {
proxy_pass http://plane-minio:9000/uploads/;
}
}
}
-4
View File
@@ -16,12 +16,8 @@
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"autoprefixer": "^10.4.15",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"tailwindcss": "^3.3.3",
"turbo": "latest"
},
"packageManager": "yarn@1.22.19"
-2
View File
@@ -16,7 +16,5 @@ module.exports = {
"no-duplicate-imports": "error",
"arrow-body-style": ["error", "as-needed"],
"react/self-closing-comp": ["error", { component: true, html: true }],
"@next/next/no-img-element": "off",
"@typescript-eslint/no-unused-vars": ["warn"],
},
};
@@ -1,10 +0,0 @@
{
"name": "tailwind-config-custom",
"version": "0.0.1",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"tailwindcss-animate": "^1.0.7"
}
}
@@ -1,7 +0,0 @@
module.exports = {
plugins: {
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
},
};
@@ -1,212 +0,0 @@
const convertToRGB = (variableName) => `rgba(var(${variableName}))`;
module.exports = {
darkMode: "class",
content: [
"./components/**/*.tsx",
"./constants/**/*.{js,ts,jsx,tsx}",
"./layouts/**/*.tsx",
"./pages/**/*.tsx",
"./ui/**/*.tsx",
],
theme: {
extend: {
boxShadow: {
"custom-shadow-2xs": "var(--color-shadow-2xs)",
"custom-shadow-xs": "var(--color-shadow-xs)",
"custom-shadow-sm": "var(--color-shadow-sm)",
"custom-shadow-rg": "var(--color-shadow-rg)",
"custom-shadow-md": "var(--color-shadow-md)",
"custom-shadow-lg": "var(--color-shadow-lg)",
"custom-shadow-xl": "var(--color-shadow-xl)",
"custom-shadow-2xl": "var(--color-shadow-2xl)",
"custom-shadow-3xl": "var(--color-shadow-3xl)",
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
},
colors: {
custom: {
primary: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-primary-10"),
20: convertToRGB("--color-primary-20"),
30: convertToRGB("--color-primary-30"),
40: convertToRGB("--color-primary-40"),
50: convertToRGB("--color-primary-50"),
60: convertToRGB("--color-primary-60"),
70: convertToRGB("--color-primary-70"),
80: convertToRGB("--color-primary-80"),
90: convertToRGB("--color-primary-90"),
100: convertToRGB("--color-primary-100"),
200: convertToRGB("--color-primary-200"),
300: convertToRGB("--color-primary-300"),
400: convertToRGB("--color-primary-400"),
500: convertToRGB("--color-primary-500"),
600: convertToRGB("--color-primary-600"),
700: convertToRGB("--color-primary-700"),
800: convertToRGB("--color-primary-800"),
900: convertToRGB("--color-primary-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-primary-100"),
},
background: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-background-10"),
20: convertToRGB("--color-background-20"),
30: convertToRGB("--color-background-30"),
40: convertToRGB("--color-background-40"),
50: convertToRGB("--color-background-50"),
60: convertToRGB("--color-background-60"),
70: convertToRGB("--color-background-70"),
80: convertToRGB("--color-background-80"),
90: convertToRGB("--color-background-90"),
100: convertToRGB("--color-background-100"),
200: convertToRGB("--color-background-200"),
300: convertToRGB("--color-background-300"),
400: convertToRGB("--color-background-400"),
500: convertToRGB("--color-background-500"),
600: convertToRGB("--color-background-600"),
700: convertToRGB("--color-background-700"),
800: convertToRGB("--color-background-800"),
900: convertToRGB("--color-background-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-background-100"),
},
text: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-text-10"),
20: convertToRGB("--color-text-20"),
30: convertToRGB("--color-text-30"),
40: convertToRGB("--color-text-40"),
50: convertToRGB("--color-text-50"),
60: convertToRGB("--color-text-60"),
70: convertToRGB("--color-text-70"),
80: convertToRGB("--color-text-80"),
90: convertToRGB("--color-text-90"),
100: convertToRGB("--color-text-100"),
200: convertToRGB("--color-text-200"),
300: convertToRGB("--color-text-300"),
400: convertToRGB("--color-text-400"),
500: convertToRGB("--color-text-500"),
600: convertToRGB("--color-text-600"),
700: convertToRGB("--color-text-700"),
800: convertToRGB("--color-text-800"),
900: convertToRGB("--color-text-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-text-100"),
},
border: {
0: "rgb(255, 255, 255)",
100: convertToRGB("--color-border-100"),
200: convertToRGB("--color-border-200"),
300: convertToRGB("--color-border-300"),
400: convertToRGB("--color-border-400"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-border-200"),
},
sidebar: {
background: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-sidebar-background-10"),
20: convertToRGB("--color-sidebar-background-20"),
30: convertToRGB("--color-sidebar-background-30"),
40: convertToRGB("--color-sidebar-background-40"),
50: convertToRGB("--color-sidebar-background-50"),
60: convertToRGB("--color-sidebar-background-60"),
70: convertToRGB("--color-sidebar-background-70"),
80: convertToRGB("--color-sidebar-background-80"),
90: convertToRGB("--color-sidebar-background-90"),
100: convertToRGB("--color-sidebar-background-100"),
200: convertToRGB("--color-sidebar-background-200"),
300: convertToRGB("--color-sidebar-background-300"),
400: convertToRGB("--color-sidebar-background-400"),
500: convertToRGB("--color-sidebar-background-500"),
600: convertToRGB("--color-sidebar-background-600"),
700: convertToRGB("--color-sidebar-background-700"),
800: convertToRGB("--color-sidebar-background-800"),
900: convertToRGB("--color-sidebar-background-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-sidebar-background-100"),
},
text: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-sidebar-text-10"),
20: convertToRGB("--color-sidebar-text-20"),
30: convertToRGB("--color-sidebar-text-30"),
40: convertToRGB("--color-sidebar-text-40"),
50: convertToRGB("--color-sidebar-text-50"),
60: convertToRGB("--color-sidebar-text-60"),
70: convertToRGB("--color-sidebar-text-70"),
80: convertToRGB("--color-sidebar-text-80"),
90: convertToRGB("--color-sidebar-text-90"),
100: convertToRGB("--color-sidebar-text-100"),
200: convertToRGB("--color-sidebar-text-200"),
300: convertToRGB("--color-sidebar-text-300"),
400: convertToRGB("--color-sidebar-text-400"),
500: convertToRGB("--color-sidebar-text-500"),
600: convertToRGB("--color-sidebar-text-600"),
700: convertToRGB("--color-sidebar-text-700"),
800: convertToRGB("--color-sidebar-text-800"),
900: convertToRGB("--color-sidebar-text-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-sidebar-text-100"),
},
border: {
0: "rgb(255, 255, 255)",
100: convertToRGB("--color-sidebar-border-100"),
200: convertToRGB("--color-sidebar-border-200"),
300: convertToRGB("--color-sidebar-border-300"),
400: convertToRGB("--color-sidebar-border-400"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-sidebar-border-200"),
},
},
backdrop: "#131313",
},
},
keyframes: {
leftToaster: {
"0%": { left: "-20rem" },
"100%": { left: "0" },
},
rightToaster: {
"0%": { right: "-20rem" },
"100%": { right: "0" },
},
},
typography: ({ theme }) => ({
brand: {
css: {
"--tw-prose-body": convertToRGB("--color-text-100"),
"--tw-prose-p": convertToRGB("--color-text-100"),
"--tw-prose-headings": convertToRGB("--color-text-100"),
"--tw-prose-lead": convertToRGB("--color-text-100"),
"--tw-prose-links": convertToRGB("--color-primary-100"),
"--tw-prose-bold": convertToRGB("--color-text-100"),
"--tw-prose-counters": convertToRGB("--color-text-100"),
"--tw-prose-bullets": convertToRGB("--color-text-100"),
"--tw-prose-hr": convertToRGB("--color-text-100"),
"--tw-prose-quotes": convertToRGB("--color-text-100"),
"--tw-prose-quote-borders": convertToRGB("--color-border"),
"--tw-prose-code": convertToRGB("--color-text-100"),
"--tw-prose-pre-code": convertToRGB("--color-text-100"),
"--tw-prose-pre-bg": convertToRGB("--color-background-100"),
"--tw-prose-th-borders": convertToRGB("--color-border"),
"--tw-prose-td-borders": convertToRGB("--color-border"),
},
},
}),
},
fontFamily: {
custom: ["Inter", "sans-serif"],
},
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
};
-1
View File
@@ -17,7 +17,6 @@
"next": "12.3.2",
"react": "^18.2.0",
"tsconfig": "*",
"tailwind-config-custom": "*",
"typescript": "4.7.4"
}
}
-1
View File
@@ -1 +0,0 @@
module.exports = require("tailwind-config-custom/postcss.config");
-1
View File
@@ -1 +0,0 @@
module.exports = require("tailwind-config-custom/tailwind.config");
+6 -2
View File
@@ -1,5 +1,9 @@
{
"extends": "tsconfig/react-library.json",
"extends": "../tsconfig/nextjs.json",
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
"exclude": ["dist", "build", "node_modules"],
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["DOM"]
}
}
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
FROM=$1
TO=$2
DIRECTORY=$3
if [ "${FROM}" = "${TO}" ]; then
echo "Nothing to replace, the value is already set to ${TO}."
exit 0
fi
# Only perform action if $FROM and $TO are different.
echo "Replacing all statically built instances of $FROM with this string $TO ."
grep -R -la "${FROM}" $DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
+11 -6
View File
@@ -5,12 +5,15 @@ cp ./.env.example ./.env
export LC_ALL=C
export LC_CTYPE=C
cp ./web/.env.example ./web/.env
cp ./space/.env.example ./space/.env
cp ./apiserver/.env.example ./apiserver/.env
# Generate the NEXT_PUBLIC_API_BASE_URL with given IP
echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env
# Generate the SECRET_KEY that will be used by django
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.env
# WEB_URL for email redirection and image saving
echo -e "WEB_URL=$1" >> ./.env
# Generate Prompt for taking tiptap auth key
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
@@ -18,7 +21,9 @@ echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token
echo -e "\e[1;38m 1. Head over to TipTap cloud's Pro Extensions Page, https://collab.tiptap.dev/pro-extensions \e[0m"
echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, after 'Here it is' \e[0m \n"
read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken
read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken
echo "@tiptap-pro:registry=https://registry.tiptap.dev/
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
+5 -1
View File
@@ -1,4 +1,8 @@
# Base url for the API requests
NEXT_PUBLIC_API_BASE_URL=""
# Public boards deploy URL
NEXT_PUBLIC_DEPLOY_URL=""
# Google Client ID for Google OAuth
NEXT_PUBLIC_GOOGLE_CLIENTID=""
# Flag to toggle OAuth
NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_ENABLE_OAUTH=1
+3
View File
@@ -1,4 +1,7 @@
module.exports = {
root: true,
extends: ["custom"],
rules: {
"@next/next/no-img-element": "off",
},
};
+11 -7
View File
@@ -1,6 +1,7 @@
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
@@ -19,16 +20,19 @@ RUN yarn install --network-timeout 500000
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
RUN yarn turbo run build --filter=space
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} space
FROM node:18-alpine AS runner
WORKDIR /app
@@ -44,14 +48,14 @@ COPY --from=installer --chown=captain:plane /app/space/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/space/.next ./space/.next
COPY --from=installer --chown=captain:plane /app/space/public ./space/public
ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
USER captain
@@ -1,6 +1,9 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// react hook form
import { useForm } from "react-hook-form";
// components
import { EmailResetPasswordForm } from "./email-reset-password-form";
+2 -2
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React from "react";
import Image from "next/image";
import { useRouter } from "next/router";
@@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
// components
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
// images
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
export const SignInView = observer(() => {
const { user: userStore } = useMobxStore();
-1
View File
@@ -1 +0,0 @@
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
+5 -1
View File
@@ -17,6 +17,7 @@
"@heroicons/react": "^2.0.12",
"@mui/icons-material": "^5.14.1",
"@mui/material": "^5.14.1",
"@tailwindcss/typography": "^0.5.9",
"@tiptap-pro/extension-unique-id": "^2.1.0",
"@tiptap/extension-code-block-lowlight": "^2.0.4",
"@tiptap/extension-color": "^2.0.4",
@@ -61,6 +62,7 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.3",
"@types/node": "18.14.1",
"@types/nprogress": "^0.2.0",
@@ -68,10 +70,12 @@
"@types/react-dom": "18.0.11",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"autoprefixer": "^10.4.13",
"eslint": "8.34.0",
"eslint-config-custom": "*",
"eslint-config-next": "13.2.1",
"postcss": "^8.4.21",
"tsconfig": "*",
"tailwind-config-custom": "*"
"tailwindcss": "^3.2.7"
}
}
+7 -1
View File
@@ -2,16 +2,22 @@ import React, { useEffect } from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// services
import authenticationService from "services/authentication.service";
// hooks
import useToast from "hooks/use-toast";
// components
import { OnBoardingForm } from "components/accounts/onboarding-form";
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
const OnBoardingPage = () => {
const { user: userStore } = useMobxStore();
const user = userStore?.currentUser;
const { setToastAlert } = useToast();
useEffect(() => {
const user = userStore?.currentUser;
+7 -1
View File
@@ -1 +1,7 @@
module.exports = require("tailwind-config-custom/postcss.config");
module.exports = {
plugins: {
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
},
};
+1 -2
View File
@@ -1,10 +1,9 @@
// services
import APIService from "services/api.service";
import { API_BASE_URL } from "helpers/common.helper";
class AuthService extends APIService {
constructor() {
super(API_BASE_URL);
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
async emailLogin(data: any) {
+4 -2
View File
@@ -1,5 +1,7 @@
// services
import APIService from "services/api.service";
import { API_BASE_URL } from "helpers/common.helper";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
interface UnSplashImage {
id: string;
@@ -27,7 +29,7 @@ interface UnSplashImageUrls {
class FileServices extends APIService {
constructor() {
super(API_BASE_URL);
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
+1 -2
View File
@@ -1,10 +1,9 @@
// services
import APIService from "services/api.service";
import { API_BASE_URL } from "helpers/common.helper";
class IssueService extends APIService {
constructor() {
super(API_BASE_URL);
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise<any> {
+1 -2
View File
@@ -1,10 +1,9 @@
// services
import APIService from "services/api.service";
import { API_BASE_URL } from "helpers/common.helper";
class ProjectService extends APIService {
constructor() {
super(API_BASE_URL);
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
async getProjectSettings(workspace_slug: string, project_slug: string): Promise<any> {
+1 -2
View File
@@ -1,10 +1,9 @@
// services
import APIService from "services/api.service";
import { API_BASE_URL } from "helpers/common.helper";
class UserService extends APIService {
constructor() {
super(API_BASE_URL);
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
async currentUser(): Promise<any> {
+203 -1
View File
@@ -1 +1,203 @@
module.exports = require("tailwind-config-custom/tailwind.config");
/** @type {import('tailwindcss').Config} */
const convertToRGB = (variableName) => `rgba(var(${variableName}))`;
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./layouts/**/*.tsx",
"./components/**/*.{js,ts,jsx,tsx}",
"./constants/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
boxShadow: {
"custom-shadow-2xs": "var(--color-shadow-2xs)",
"custom-shadow-xs": "var(--color-shadow-xs)",
"custom-shadow-sm": "var(--color-shadow-sm)",
"custom-shadow-rg": "var(--color-shadow-rg)",
"custom-shadow-md": "var(--color-shadow-md)",
"custom-shadow-lg": "var(--color-shadow-lg)",
"custom-shadow-xl": "var(--color-shadow-xl)",
"custom-shadow-2xl": "var(--color-shadow-2xl)",
"custom-shadow-3xl": "var(--color-shadow-3xl)",
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
},
colors: {
custom: {
primary: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-primary-10"),
20: convertToRGB("--color-primary-20"),
30: convertToRGB("--color-primary-30"),
40: convertToRGB("--color-primary-40"),
50: convertToRGB("--color-primary-50"),
60: convertToRGB("--color-primary-60"),
70: convertToRGB("--color-primary-70"),
80: convertToRGB("--color-primary-80"),
90: convertToRGB("--color-primary-90"),
100: convertToRGB("--color-primary-100"),
200: convertToRGB("--color-primary-200"),
300: convertToRGB("--color-primary-300"),
400: convertToRGB("--color-primary-400"),
500: convertToRGB("--color-primary-500"),
600: convertToRGB("--color-primary-600"),
700: convertToRGB("--color-primary-700"),
800: convertToRGB("--color-primary-800"),
900: convertToRGB("--color-primary-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-primary-100"),
},
background: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-background-10"),
20: convertToRGB("--color-background-20"),
30: convertToRGB("--color-background-30"),
40: convertToRGB("--color-background-40"),
50: convertToRGB("--color-background-50"),
60: convertToRGB("--color-background-60"),
70: convertToRGB("--color-background-70"),
80: convertToRGB("--color-background-80"),
90: convertToRGB("--color-background-90"),
100: convertToRGB("--color-background-100"),
200: convertToRGB("--color-background-200"),
300: convertToRGB("--color-background-300"),
400: convertToRGB("--color-background-400"),
500: convertToRGB("--color-background-500"),
600: convertToRGB("--color-background-600"),
700: convertToRGB("--color-background-700"),
800: convertToRGB("--color-background-800"),
900: convertToRGB("--color-background-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-background-100"),
},
text: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-text-10"),
20: convertToRGB("--color-text-20"),
30: convertToRGB("--color-text-30"),
40: convertToRGB("--color-text-40"),
50: convertToRGB("--color-text-50"),
60: convertToRGB("--color-text-60"),
70: convertToRGB("--color-text-70"),
80: convertToRGB("--color-text-80"),
90: convertToRGB("--color-text-90"),
100: convertToRGB("--color-text-100"),
200: convertToRGB("--color-text-200"),
300: convertToRGB("--color-text-300"),
400: convertToRGB("--color-text-400"),
500: convertToRGB("--color-text-500"),
600: convertToRGB("--color-text-600"),
700: convertToRGB("--color-text-700"),
800: convertToRGB("--color-text-800"),
900: convertToRGB("--color-text-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-text-100"),
},
border: {
0: "rgb(255, 255, 255)",
100: convertToRGB("--color-border-100"),
200: convertToRGB("--color-border-200"),
300: convertToRGB("--color-border-300"),
400: convertToRGB("--color-border-400"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-border-200"),
},
sidebar: {
background: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-sidebar-background-10"),
20: convertToRGB("--color-sidebar-background-20"),
30: convertToRGB("--color-sidebar-background-30"),
40: convertToRGB("--color-sidebar-background-40"),
50: convertToRGB("--color-sidebar-background-50"),
60: convertToRGB("--color-sidebar-background-60"),
70: convertToRGB("--color-sidebar-background-70"),
80: convertToRGB("--color-sidebar-background-80"),
90: convertToRGB("--color-sidebar-background-90"),
100: convertToRGB("--color-sidebar-background-100"),
200: convertToRGB("--color-sidebar-background-200"),
300: convertToRGB("--color-sidebar-background-300"),
400: convertToRGB("--color-sidebar-background-400"),
500: convertToRGB("--color-sidebar-background-500"),
600: convertToRGB("--color-sidebar-background-600"),
700: convertToRGB("--color-sidebar-background-700"),
800: convertToRGB("--color-sidebar-background-800"),
900: convertToRGB("--color-sidebar-background-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-sidebar-background-100"),
},
text: {
0: "rgb(255, 255, 255)",
10: convertToRGB("--color-sidebar-text-10"),
20: convertToRGB("--color-sidebar-text-20"),
30: convertToRGB("--color-sidebar-text-30"),
40: convertToRGB("--color-sidebar-text-40"),
50: convertToRGB("--color-sidebar-text-50"),
60: convertToRGB("--color-sidebar-text-60"),
70: convertToRGB("--color-sidebar-text-70"),
80: convertToRGB("--color-sidebar-text-80"),
90: convertToRGB("--color-sidebar-text-90"),
100: convertToRGB("--color-sidebar-text-100"),
200: convertToRGB("--color-sidebar-text-200"),
300: convertToRGB("--color-sidebar-text-300"),
400: convertToRGB("--color-sidebar-text-400"),
500: convertToRGB("--color-sidebar-text-500"),
600: convertToRGB("--color-sidebar-text-600"),
700: convertToRGB("--color-sidebar-text-700"),
800: convertToRGB("--color-sidebar-text-800"),
900: convertToRGB("--color-sidebar-text-900"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-sidebar-text-100"),
},
border: {
0: "rgb(255, 255, 255)",
100: convertToRGB("--color-sidebar-border-100"),
200: convertToRGB("--color-sidebar-border-200"),
300: convertToRGB("--color-sidebar-border-300"),
400: convertToRGB("--color-sidebar-border-400"),
1000: "rgb(0, 0, 0)",
DEFAULT: convertToRGB("--color-sidebar-border-200"),
},
},
backdrop: "#131313",
},
},
typography: ({ theme }) => ({
brand: {
css: {
"--tw-prose-body": convertToRGB("--color-text-100"),
"--tw-prose-p": convertToRGB("--color-text-100"),
"--tw-prose-headings": convertToRGB("--color-text-100"),
"--tw-prose-lead": convertToRGB("--color-text-100"),
"--tw-prose-links": convertToRGB("--color-primary-100"),
"--tw-prose-bold": convertToRGB("--color-text-100"),
"--tw-prose-counters": convertToRGB("--color-text-100"),
"--tw-prose-bullets": convertToRGB("--color-text-100"),
"--tw-prose-hr": convertToRGB("--color-text-100"),
"--tw-prose-quotes": convertToRGB("--color-text-100"),
"--tw-prose-quote-borders": convertToRGB("--color-border"),
"--tw-prose-code": convertToRGB("--color-text-100"),
"--tw-prose-pre-code": convertToRGB("--color-text-100"),
"--tw-prose-pre-bg": convertToRGB("--color-background-100"),
"--tw-prose-th-borders": convertToRGB("--color-border"),
"--tw-prose-td-borders": convertToRGB("--color-border"),
},
},
}),
},
fontFamily: {
custom: ["Inter", "sans-serif"],
},
},
plugins: [require("@tailwindcss/typography")],
};
+4
View File
@@ -1,5 +1,9 @@
#!/bin/sh
set -x
# Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL
# NOTE: if these values are the same, this will be skipped.
/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" $2
echo "Starting Plane Frontend.."
node $1
+7 -10
View File
@@ -15,20 +15,17 @@
"NEXT_PUBLIC_UNSPLASH_ACCESS",
"NEXT_PUBLIC_UNSPLASH_ENABLED",
"NEXT_PUBLIC_TRACK_EVENTS",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"TRACKER_ACCESS_KEY",
"NEXT_PUBLIC_CRISP_ID",
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
"NEXT_PUBLIC_DEPLOY_WITH_NGINX",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"SLACK_OAUTH_URL",
"SLACK_CLIENT_ID",
"SLACK_CLIENT_SECRET",
"JITSU_TRACKER_ACCESS_KEY",
"JITSU_TRACKER_HOST",
"UNSPLASH_ACCESS_KEY"
"NEXT_PUBLIC_SLACK_CLIENT_ID",
"NEXT_PUBLIC_SLACK_CLIENT_SECRET",
"NEXT_PUBLIC_SUPABASE_URL",
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_DEPLOY_WITH_NGINX"
],
"pipeline": {
"build": {
+3 -1
View File
@@ -1,3 +1,5 @@
# Base url for the API requests
NEXT_PUBLIC_API_BASE_URL=""
# Extra image domains that need to be added for Next Image
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
# Google Client ID for Google OAuth
@@ -21,4 +23,4 @@ NEXT_PUBLIC_SLACK_CLIENT_ID=""
# For Telemetry, set it to "app.plane.so"
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
# Public boards deploy URL
NEXT_PUBLIC_DEPLOY_URL="http://localhost:3000/spaces"
NEXT_PUBLIC_DEPLOY_URL=""
+3
View File
@@ -1,4 +1,7 @@
module.exports = {
root: true,
extends: ["custom"],
rules: {
"@next/next/no-img-element": "off",
},
};
+20 -8
View File
@@ -2,6 +2,7 @@ FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
@@ -13,8 +14,8 @@ FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_DEPLOY_URL=""
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
@@ -25,12 +26,18 @@ RUN yarn install --network-timeout 500000
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
RUN chmod +x /usr/local/bin/replace-env-vars.sh
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
RUN yarn turbo run build --filter=web
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} web
FROM node:18-alpine AS runner
WORKDIR /app
@@ -45,15 +52,20 @@ COPY --from=installer /app/web/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=captain:plane /app/web/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/web/.next ./web/.next
ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_DEPLOY_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
USER captain
@@ -3,8 +3,8 @@ import React, { useState } from "react";
// component
import { CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icon
import { ArchiveRestore } from "lucide-react";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
// constants
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
// types
@@ -28,18 +28,14 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
handleClose={() => setmonthModal(false)}
handleChange={handleChange}
/>
<div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
<div className="flex items-center justify-between">
<div className="flex items-start gap-3">
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
<ArchiveRestore className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
</div>
<div className="">
<h4 className="text-sm font-medium">Auto-archive closed issues</h4>
<p className="text-sm text-custom-text-200 tracking-tight">
Plane will auto archive issues that have been completed or canceled.
</p>
</div>
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 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}
@@ -51,43 +47,40 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
size="sm"
/>
</div>
{projectDetails?.archive_in !== 0 && (
<div className="ml-12">
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
<div className="w-1/2 text-sm 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="bottom"
width="w-full"
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
<span className="text-sm">{month.label}</span>
</CustomSelect.Option>
))}
<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 text-sm select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
onClick={() => setmonthModal(true)}
>
Customise Time Range
</button>
</>
</CustomSelect>
</div>
<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>
)}
@@ -5,12 +5,11 @@ import useSWR from "swr";
import { useRouter } from "next/router";
// component
import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui";
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icons
import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { StateGroupIcon } from "components/icons";
import { ArchiveX } from "lucide-react";
// services
import stateService from "services/state.service";
// constants
@@ -77,18 +76,14 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
handleChange={handleChange}
/>
<div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
<div className="flex items-center justify-between">
<div className="flex items-start gap-3">
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
<ArchiveX className="h-4 w-4 text-red-500 flex-shrink-0" />
</div>
<div className="">
<h4 className="text-sm font-medium">Auto-close issues</h4>
<p className="text-sm text-custom-text-200 tracking-tight">
Plane will automatically close issue that havent been completed or canceled.
</p>
</div>
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 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}
@@ -100,86 +95,82 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
size="sm"
/>
</div>
{projectDetails?.close_in !== 0 && (
<div className="ml-12">
<div className="flex flex-col rounded bg-custom-background-90 border border-custom-border-200 p-2">
<div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
<div className="w-1/2 text-sm 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)}
>
Customise Time Range
</button>
</>
</CustomSelect>
</div>
<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="flex items-center justify-between px-5 py-4 gap-2 w-full">
<div className="w-1/2 text-sm 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 ? (
<StateGroupIcon
stateGroup={selectedOption.group}
color={selectedOption.color}
height="16px"
width="16px"
/>
) : currentDefaultState ? (
<StateGroupIcon
stateGroup={currentDefaultState.group}
color={currentDefaultState.color}
height="16px"
width="16px"
/>
) : (
<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 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 ? (
<StateGroupIcon
stateGroup={selectedOption.group}
color={selectedOption.color}
height="16px"
width="16px"
/>
) : currentDefaultState ? (
<StateGroupIcon
stateGroup={currentDefaultState.group}
color={currentDefaultState.color}
height="16px"
width="16px"
/>
) : (
<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>
@@ -104,7 +104,7 @@ export const SelectMonthModal: React.FC<Props> = ({
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Customise Time Range
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">
@@ -41,7 +41,7 @@ export const CommandPalette: React.FC = observer(() => {
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, issueId, inboxId, cycleId, moduleId } = router.query;
const { workspaceSlug, projectId, issueId, inboxId } = router.query;
const { user } = useUser();
@@ -183,13 +183,6 @@ export const CommandPalette: React.FC = observer(() => {
isOpen={isIssueModalOpen}
handleClose={() => setIsIssueModalOpen(false)}
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
prePopulateData={
cycleId
? { cycle: cycleId.toString() }
: moduleId
? { module: moduleId.toString() }
: undefined
}
/>
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssuesModalOpen}
+18 -57
View File
@@ -2,9 +2,8 @@ import { useRouter } from "next/router";
// icons
import { Icon, Tooltip } from "components/ui";
import { CopyPlus } from "lucide-react";
import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons";
import { BlockedIcon, BlockerIcon } from "components/icons";
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper";
@@ -91,14 +90,14 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
},
archived_at: {
message: (activity) => {
if (activity.new_value === "restore") return "restored the issue.";
else return "archived the issue.";
},
icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
},
attachment: {
message: (activity, showIssue) => {
@@ -137,7 +136,7 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
},
blocking: {
message: (activity) => {
@@ -158,7 +157,7 @@ const activityDetails: {
},
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
},
blocked_by: {
blocks: {
message: (activity) => {
if (activity.old_value === "")
return (
@@ -177,44 +176,6 @@ const activityDetails: {
},
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
},
duplicate: {
message: (activity) => {
if (activity.old_value === "")
return (
<>
marked this issue as duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed this issue as a duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
icon: <CopyPlus size={12} color="#6b7280" />,
},
relates_to: {
message: (activity) => {
if (activity.old_value === "")
return (
<>
marked that this issue relates to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed the relation from{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
},
cycles: {
message: (activity, showIssue, workspaceSlug) => {
if (activity.verb === "created")
@@ -263,7 +224,7 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
},
description: {
message: (activity, showIssue) => (
@@ -278,7 +239,7 @@ const activityDetails: {
.
</>
),
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
},
estimate_point: {
message: (activity, showIssue) => {
@@ -310,14 +271,14 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
},
issue: {
message: (activity) => {
if (activity.verb === "created") return "created the issue.";
else return "deleted an issue.";
},
icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
},
labels: {
message: (activity, showIssue) => {
@@ -366,7 +327,7 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
},
link: {
message: (activity, showIssue) => {
@@ -437,7 +398,7 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
},
modules: {
message: (activity, showIssue, workspaceSlug) => {
@@ -487,7 +448,7 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
},
name: {
message: (activity, showIssue) => (
@@ -502,7 +463,7 @@ const activityDetails: {
.
</>
),
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
},
parent: {
message: (activity, showIssue) => {
@@ -535,7 +496,7 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
},
priority: {
message: (activity, showIssue) => (
@@ -553,7 +514,7 @@ const activityDetails: {
.
</>
),
icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
},
start_date: {
message: (activity, showIssue) => {
@@ -587,7 +548,7 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
},
state: {
message: (activity, showIssue) => (
@@ -603,7 +564,7 @@ const activityDetails: {
.
</>
),
icon: <Squares2X2Icon className="h-6 w-6 text-custom-sidebar-200" aria-hidden="true" />,
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />,
},
target_date: {
message: (activity, showIssue) => {
@@ -637,7 +598,7 @@ const activityDetails: {
</>
);
},
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
},
};
@@ -52,26 +52,22 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
},
];
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [
{
type: "list",
Icon: FormatListBulletedOutlined,
},
{
type: "kanban",
Icon: GridViewOutlined,
},
];
export const IssuesFilterView: React.FC = () => {
const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname.includes("draft-issues");
const {
displayFilters,
setDisplayFilters,
issueView,
setIssueView,
groupByProperty,
setGroupByProperty,
orderBy,
setOrderBy,
showEmptyGroups,
showSubIssues,
setShowSubIssues,
setShowEmptyGroups,
filters,
setFilters,
resetFilterToDefault,
@@ -87,7 +83,7 @@ export const IssuesFilterView: React.FC = () => {
return (
<div className="flex items-center gap-2">
{!isArchivedIssues && !isDraftIssues && (
{!isArchivedIssues && (
<div className="flex items-center gap-x-1">
{issueViewOptions.map((option) => (
<Tooltip
@@ -100,41 +96,11 @@ export const IssuesFilterView: React.FC = () => {
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
displayFilters.layout === option.type
issueView === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
}`}
onClick={() => setDisplayFilters({ layout: option.type })}
>
<option.Icon
sx={{
fontSize: 16,
}}
className={option.type === "gantt_chart" ? "rotate-90" : ""}
/>
</button>
</Tooltip>
))}
</div>
)}
{isDraftIssues && (
<div className="flex items-center gap-x-1">
{issueViewForDraftIssues.map((option) => (
<Tooltip
key={option.type}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
position="bottom"
>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
displayFilters.layout === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
}`}
onClick={() => setDisplayFilters({ layout: option.type })}
onClick={() => setIssueView(option.type)}
>
<option.Icon
sx={{
@@ -208,30 +174,28 @@ export const IssuesFilterView: React.FC = () => {
<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-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" &&
displayFilters.layout !== "gantt_chart" && (
{issueView !== "calendar" &&
issueView !== "spreadsheet" &&
issueView !== "gantt_chart" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
GROUP_BY_OPTIONS.find(
(option) => option.key === displayFilters.group_by
)?.name ?? "Select"
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (displayFilters.layout === "kanban" && option.key === null)
return null;
if (issueView === "kanban" && option.key === null) return null;
if (option.key === "project") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })}
onClick={() => setGroupByProperty(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
@@ -241,45 +205,41 @@ export const IssuesFilterView: React.FC = () => {
</div>
</div>
)}
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find(
(option) => option.key === displayFilters.order_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) =>
displayFilters.group_by === "priority" &&
option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
)}
</div>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find(
(option) => option.key === displayFilters.type
)?.name ?? "Select"
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
@@ -288,7 +248,7 @@ export const IssuesFilterView: React.FC = () => {
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setDisplayFilters({
setFilters({
type: option.key,
})
}
@@ -300,40 +260,33 @@ export const IssuesFilterView: React.FC = () => {
</div>
</div>
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show sub-issues</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters.sub_issue ?? true}
onChange={() =>
setDisplayFilters({ sub_issue: !displayFilters.sub_issue })
}
/>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show sub-issues</h4>
<div className="w-28">
<ToggleSwitch
value={showSubIssues}
onChange={() => setShowSubIssues(!showSubIssues)}
/>
</div>
)}
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" &&
displayFilters.layout !== "gantt_chart" && (
</div>
)}
{issueView !== "calendar" &&
issueView !== "spreadsheet" &&
issueView !== "gantt_chart" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters.show_empty_groups,
})
}
value={showEmptyGroups}
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
/>
</div>
</div>
)}
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" &&
displayFilters.layout !== "gantt_chart" && (
{issueView !== "calendar" &&
issueView !== "spreadsheet" &&
issueView !== "gantt_chart" && (
<div className="relative flex justify-end gap-x-3">
<button type="button" onClick={() => resetFilterToDefault()}>
Reset to default
@@ -349,7 +302,7 @@ export const IssuesFilterView: React.FC = () => {
)}
</div>
{displayFilters.layout !== "gantt_chart" && (
{issueView !== "gantt_chart" && (
<div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
@@ -357,7 +310,7 @@ export const IssuesFilterView: React.FC = () => {
if (key === "estimate" && !isEstimateActive) return null;
if (
displayFilters.layout === "spreadsheet" &&
issueView === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
@@ -365,7 +318,7 @@ export const IssuesFilterView: React.FC = () => {
return null;
if (
displayFilters.layout !== "spreadsheet" &&
issueView !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;
+2 -10
View File
@@ -20,7 +20,6 @@ import fileService from "services/file.service";
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
// hooks
import useWorkspaceDetails from "hooks/use-workspace-details";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
const unsplashEnabled =
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
@@ -68,8 +67,6 @@ export const ImagePickerPopover: React.FC<Props> = ({
fileService.getUnsplashImages(1, searchParams)
);
const imagePickerRef = useRef<HTMLDivElement>(null);
const { workspaceDetails } = useWorkspaceDetails();
const onDrop = useCallback((acceptedFiles: File[]) => {
@@ -119,14 +116,12 @@ export const ImagePickerPopover: React.FC<Props> = ({
onChange(images[0].urls.regular);
}, [value, onChange, images]);
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
if (!unsplashEnabled) return null;
return (
<Popover className="relative z-[2]" ref={ref}>
<Popover.Button
className="rounded-sm border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
className="rounded-md border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
onClick={() => setIsOpen((prev) => !prev)}
disabled={disabled}
>
@@ -142,10 +137,7 @@ export const ImagePickerPopover: React.FC<Props> = ({
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
<div
ref={imagePickerRef}
className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl"
>
<div className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl">
<Tab.Group>
<div>
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
@@ -58,7 +58,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
);
const { setToastAlert } = useToast();
const { displayFilters, params } = useIssuesView();
const { issueView, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params;
@@ -126,8 +126,8 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
message: "Issues deleted successfully!",
});
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
if (issueView === "calendar") mutate(calendarFetchKey);
else if (issueView === "gantt_chart") mutate(ganttFetchKey);
else {
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
@@ -1,5 +1,6 @@
import React, { useCallback, useState } from "react";
import NextImage from "next/image";
import { useRouter } from "next/router";
// react-dropzone
@@ -11,7 +12,7 @@ import fileServices from "services/file.service";
// hooks
import useWorkspaceDetails from "hooks/use-workspace-details";
// ui
import { DangerButton, PrimaryButton, SecondaryButton } from "components/ui";
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { UserCircleIcon } from "components/icons";
@@ -20,8 +21,6 @@ type Props = {
onClose: () => void;
isOpen: boolean;
onSuccess: (url: string) => void;
isRemoving: boolean;
handleDelete: () => void;
userImage?: boolean;
};
@@ -30,8 +29,6 @@ export const ImageUploadModal: React.FC<Props> = ({
onSuccess,
isOpen,
onClose,
isRemoving,
handleDelete,
userImage,
}) => {
const [image, setImage] = useState<File | null>(null);
@@ -151,10 +148,12 @@ export const ImageUploadModal: React.FC<Props> = ({
>
Edit
</button>
<img
<NextImage
layout="fill"
objectFit="cover"
src={image ? URL.createObjectURL(image) : value ? value : ""}
alt="image"
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
className="rounded-lg"
/>
</>
) : (
@@ -183,22 +182,15 @@ export const ImageUploadModal: React.FC<Props> = ({
<p className="my-4 text-custom-text-200 text-sm">
File formats supported- .jpeg, .jpg, .png, .webp, .svg
</p>
<div className="flex items-center justify-between">
<div className="flex items-center">
<DangerButton onClick={handleDelete} outline disabled={!value}>
{isRemoving ? "Removing..." : "Remove"}
</DangerButton>
</div>
<div className="flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton
onClick={handleSubmit}
disabled={!image}
loading={isImageUploading}
>
{isImageUploading ? "Uploading..." : "Upload & Save"}
</PrimaryButton>
</div>
<div className="flex items-center justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton
onClick={handleSubmit}
disabled={!image}
loading={isImageUploading}
>
{isImageUploading ? "Uploading..." : "Upload & Save"}
</PrimaryButton>
</div>
</Dialog.Panel>
</Transition.Child>
+10 -26
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react";
import React, { useCallback } from "react";
import { useRouter } from "next/router";
@@ -50,7 +50,6 @@ type Props = {
secondaryButton?: React.ReactNode;
};
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
handleOnDragEnd: (result: DropResult) => Promise<void>;
openIssuesListModal: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
@@ -67,7 +66,6 @@ export const AllViews: React.FC<Props> = ({
dragDisabled = false,
emptyState,
handleIssueAction,
handleDraftIssueAction,
handleOnDragEnd,
openIssuesListModal,
removeIssue,
@@ -79,12 +77,10 @@ export const AllViews: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const [myIssueProjectId, setMyIssueProjectId] = useState<string | null>(null);
const { user } = useUser();
const { memberRole } = useProjectMyMembership();
const { groupedIssues, isEmpty, displayFilters } = viewProps;
const { groupedIssues, isEmpty, issueView } = viewProps;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
@@ -94,10 +90,6 @@ export const AllViews: React.FC<Props> = ({
);
const states = getStatesList(stateGroups);
const handleMyIssueOpen = (issue: IIssue) => {
setMyIssueProjectId(issue.project);
};
const handleTrashBox = useCallback(
(isDragging: boolean) => {
if (isDragging && !trashBox) setTrashBox(true);
@@ -125,45 +117,39 @@ export const AllViews: React.FC<Props> = ({
</StrictModeDroppable>
{groupedIssues ? (
!isEmpty ||
displayFilters?.layout === "kanban" ||
displayFilters?.layout === "calendar" ||
displayFilters?.layout === "gantt_chart" ? (
issueView === "kanban" ||
issueView === "calendar" ||
issueView === "gantt_chart" ? (
<>
{displayFilters?.layout === "list" ? (
{issueView === "list" ? (
<AllLists
states={states}
addIssueToGroup={addIssueToGroup}
handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
removeIssue={removeIssue}
myIssueProjectId={myIssueProjectId}
handleMyIssueOpen={handleMyIssueOpen}
disableUserActions={disableUserActions}
disableAddIssueOption={disableAddIssueOption}
user={user}
userAuth={memberRole}
viewProps={viewProps}
/>
) : displayFilters?.layout === "kanban" ? (
) : issueView === "kanban" ? (
<AllBoards
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
disableAddIssueOption={disableAddIssueOption}
dragDisabled={dragDisabled}
handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
handleTrashBox={handleTrashBox}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
myIssueProjectId={myIssueProjectId}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={removeIssue}
states={states}
user={user}
userAuth={memberRole}
viewProps={viewProps}
/>
) : displayFilters?.layout === "calendar" ? (
) : issueView === "calendar" ? (
<CalendarView
handleIssueAction={handleIssueAction}
addIssueToDate={addIssueToDate}
@@ -171,7 +157,7 @@ export const AllViews: React.FC<Props> = ({
user={user}
userAuth={memberRole}
/>
) : displayFilters?.layout === "spreadsheet" ? (
) : issueView === "spreadsheet" ? (
<SpreadsheetView
handleIssueAction={handleIssueAction}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
@@ -180,9 +166,7 @@ export const AllViews: React.FC<Props> = ({
userAuth={memberRole}
/>
) : (
displayFilters?.layout === "gantt_chart" && (
<GanttChartView disableUserActions={disableUserActions} />
)
issueView === "gantt_chart" && <GanttChartView />
)}
</>
) : router.pathname.includes("archived-issues") ? (
@@ -1,12 +1,5 @@
import { useRouter } from "next/router";
//hook
import useMyIssues from "hooks/my-issues/use-my-issues";
import useIssuesView from "hooks/use-issues-view";
import useProfileIssues from "hooks/use-profile-issues";
// components
import { SingleBoard } from "components/core/views/board-view/single-board";
import { IssuePeekOverview } from "components/issues";
// icons
import { StateGroupIcon } from "components/icons";
// helpers
@@ -20,12 +13,9 @@ type Props = {
disableAddIssueOption?: boolean;
dragDisabled: boolean;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
myIssueProjectId?: string | null;
handleMyIssueOpen?: (issue: IIssue) => void;
states: IState[] | undefined;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
@@ -38,53 +28,25 @@ export const AllBoards: React.FC<Props> = ({
disableAddIssueOption = false,
dragDisabled,
handleIssueAction,
handleDraftIssueAction,
handleTrashBox,
openIssuesListModal,
myIssueProjectId,
handleMyIssueOpen,
removeIssue,
states,
user,
userAuth,
viewProps,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, userId } = router.query;
const isProfileIssue =
router.pathname.includes("assigned") ||
router.pathname.includes("created") ||
router.pathname.includes("subscribed");
const isMyIssue = router.pathname.includes("my-issues");
const { mutateIssues } = useIssuesView();
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { mutateProfileIssues } = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
const { displayFilters, groupedIssues } = viewProps;
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
return (
<>
<IssuePeekOverview
handleMutation={() =>
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
}
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
{groupedIssues ? (
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
return null;
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
return (
<SingleBoard
@@ -96,10 +58,8 @@ export const AllBoards: React.FC<Props> = ({
dragDisabled={dragDisabled}
groupTitle={singleGroup}
handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
handleTrashBox={handleTrashBox}
openIssuesListModal={openIssuesListModal ?? null}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={removeIssue}
user={user}
userAuth={userAuth}
@@ -107,15 +67,13 @@ export const AllBoards: React.FC<Props> = ({
/>
);
})}
{!displayFilters?.show_empty_groups && (
{!showEmptyGroups && (
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
<h2 className="text-lg font-semibold">Hidden groups</h2>
<div className="space-y-3">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (groupedIssues[singleGroup].length === 0)
return (
@@ -133,7 +91,7 @@ export const AllBoards: React.FC<Props> = ({
/>
)}
<h4 className="text-sm capitalize">
{displayFilters?.group_by === "state"
{selectedGroup === "state"
? addSpaceIfCamelCase(currentState?.name ?? "")
: addSpaceIfCamelCase(singleGroup)}
</h4>
@@ -48,28 +48,22 @@ export const BoardHeader: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { displayFilters, groupedIssues } = viewProps;
console.log("dF", displayFilters);
const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
const { data: issueLabels } = useSWR(
workspaceSlug && projectId && displayFilters?.group_by === "labels"
workspaceSlug && projectId && selectedGroup === "labels"
? PROJECT_ISSUE_LABELS(projectId.toString())
: null,
workspaceSlug && projectId && displayFilters?.group_by === "labels"
workspaceSlug && projectId && selectedGroup === "labels"
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: members } = useSWR(
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
? PROJECT_MEMBERS(projectId.toString())
: null,
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
: null
);
@@ -79,7 +73,7 @@ export const BoardHeader: React.FC<Props> = ({
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
switch (displayFilters?.group_by) {
switch (selectedGroup) {
case "state":
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
@@ -103,7 +97,7 @@ export const BoardHeader: React.FC<Props> = ({
const getGroupIcon = () => {
let icon;
switch (displayFilters?.group_by) {
switch (selectedGroup) {
case "state":
icon = currentState && (
<StateGroupIcon
@@ -173,7 +167,7 @@ export const BoardHeader: React.FC<Props> = ({
<span className="flex items-center">{getGroupIcon()}</span>
<h2
className={`text-lg font-semibold truncate ${
displayFilters?.group_by === "created_by" ? "" : "capitalize"
selectedGroup === "created_by" ? "" : "capitalize"
}`}
style={{
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
@@ -204,7 +198,7 @@ export const BoardHeader: React.FC<Props> = ({
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
)}
</button>
{!disableAddIssue && !disableUserActions && displayFilters?.group_by !== "created_by" && (
{!disableAddIssue && !disableUserActions && selectedGroup !== "created_by" && (
<button
type="button"
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"
@@ -24,10 +24,8 @@ type Props = {
dragDisabled: boolean;
groupTitle: string;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null;
handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
@@ -42,10 +40,8 @@ export const SingleBoard: React.FC<Props> = ({
disableAddIssueOption = false,
dragDisabled,
handleIssueAction,
handleDraftIssueAction,
handleTrashBox,
openIssuesListModal,
handleMyIssueOpen,
removeIssue,
user,
userAuth,
@@ -54,7 +50,7 @@ export const SingleBoard: React.FC<Props> = ({
// collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true);
const { displayFilters, groupedIssues } = viewProps;
const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps;
const router = useRouter();
const { cycleId, moduleId } = router.query;
@@ -84,14 +80,14 @@ export const SingleBoard: React.FC<Props> = ({
{(provided, snapshot) => (
<div
className={`relative h-full ${
displayFilters?.order_by !== "sort_order" && snapshot.isDraggingOver
orderBy !== "sort_order" && snapshot.isDraggingOver
? "bg-custom-background-100/20"
: ""
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
ref={provided.innerRef}
{...provided.droppableProps}
>
{displayFilters?.order_by !== "sort_order" && (
{orderBy !== "sort_order" && (
<>
<div
className={`absolute ${
@@ -105,11 +101,7 @@ export const SingleBoard: React.FC<Props> = ({
>
This board is ordered by{" "}
{replaceUnderscoreIfSnakeCase(
displayFilters?.order_by
? displayFilters?.order_by[0] === "-"
? displayFilters?.order_by.slice(1)
: displayFilters?.order_by
: "created_at"
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
)}
</div>
</>
@@ -138,18 +130,7 @@ export const SingleBoard: React.FC<Props> = ({
editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleDraftIssueEdit={
handleDraftIssueAction
? () => handleDraftIssueAction(issue, "edit")
: undefined
}
handleDraftIssueDelete={() =>
handleDraftIssueAction
? handleDraftIssueAction(issue, "delete")
: undefined
}
handleTrashBox={handleTrashBox}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => {
if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
@@ -164,13 +145,13 @@ export const SingleBoard: React.FC<Props> = ({
))}
<span
style={{
display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
display: orderBy === "sort_order" ? "inline" : "none",
}}
>
<>{provided.placeholder}</>
{provided.placeholder}
</span>
</div>
{displayFilters?.group_by !== "created_by" && (
{selectedGroup !== "created_by" && (
<div>
{type === "issue"
? !disableAddIssueOption && (
@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
@@ -57,11 +58,8 @@ type Props = {
index: number;
editIssue: () => void;
makeIssueCopy: () => void;
handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleDraftIssueEdit?: () => void;
handleDraftIssueDelete?: () => void;
handleTrashBox: (isDragging: boolean) => void;
disableUserActions: boolean;
user: ICurrentUserResponse | undefined;
@@ -77,12 +75,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
index,
editIssue,
makeIssueCopy,
handleMyIssueOpen,
removeIssue,
groupTitle,
handleDeleteIssue,
handleDraftIssueEdit,
handleDraftIssueDelete,
handleTrashBox,
disableUserActions,
user,
@@ -98,13 +93,11 @@ export const SingleBoardIssue: React.FC<Props> = ({
const actionSectionRef = useRef<HTMLDivElement | null>(null);
const { displayFilters, properties, mutateIssues } = viewProps;
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const isDraftIssue = router.pathname.includes("draft-issues");
const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback(
@@ -138,9 +131,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
handleIssuesMutation(
formData,
groupTitle ?? "",
displayFilters?.group_by ?? null,
selectedGroup,
index,
displayFilters?.order_by ?? "-created_at",
orderBy,
prevData
),
false
@@ -156,14 +149,24 @@ export const SingleBoardIssue: React.FC<Props> = ({
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
},
[displayFilters, workspaceSlug, cycleId, moduleId, groupTitle, index, mutateIssues, user]
[
workspaceSlug,
cycleId,
moduleId,
groupTitle,
index,
selectedGroup,
mutateIssues,
orderBy,
user,
]
);
const getStyle = (
style: DraggingStyle | NotDraggingStyle | undefined,
snapshot: DraggableStateSnapshot
) => {
if (displayFilters?.order_by === "sort_order") return style;
if (orderBy === "sort_order") return style;
if (!snapshot.isDragging) return {};
if (!snapshot.isDropAnimating) return style;
@@ -194,17 +197,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
const openPeekOverview = () => {
const { query } = router;
if (handleMyIssueOpen) handleMyIssueOpen(issue);
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
return (
@@ -217,47 +209,29 @@ export const SingleBoardIssue: React.FC<Props> = ({
>
{!isNotAllowed && (
<>
<ContextMenu.Item
Icon={PencilIcon}
onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else editIssue();
}}
>
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
Edit issue
</ContextMenu.Item>
{!isDraftIssue && (
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy...
</ContextMenu.Item>
)}
<ContextMenu.Item
Icon={TrashIcon}
onClick={() => {
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
else handleDeleteIssue(issue);
}}
>
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy...
</ContextMenu.Item>
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
Delete issue
</ContextMenu.Item>
</>
)}
{!isDraftIssue && (
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
</ContextMenu.Item>
<a
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
target="_blank"
rel="noreferrer noopener"
>
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item>
)}
{!isDraftIssue && (
<a
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
target="_blank"
rel="noreferrer noopener"
>
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item>
</a>
)}
</a>
</ContextMenu>
<div
className={`mb-3 rounded bg-custom-background-100 shadow ${
@@ -292,18 +266,13 @@ export const SingleBoardIssue: React.FC<Props> = ({
</button>
}
>
<CustomMenu.MenuItem
onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else editIssue();
}}
>
<CustomMenu.MenuItem onClick={editIssue}>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && !isDraftIssue && (
{type !== "issue" && removeIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<div className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
@@ -311,48 +280,32 @@ export const SingleBoardIssue: React.FC<Props> = ({
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={() => {
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
else handleDeleteIssue(issue);
}}
>
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
{!isDraftIssue && (
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue Link</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue Link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
)}
<div className="flex flex-col gap-1.5">
{properties.key && (
<div className="text-xs font-medium text-custom-text-200">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
)}
<button
type="button"
className="text-sm text-left break-words line-clamp-2"
onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else openPeekOverview();
}}
>
{issue.name}
</button>
</div>
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a className="flex flex-col gap-1.5">
{properties.key && (
<div className="text-xs font-medium text-custom-text-200">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
)}
<h5 className="text-sm break-words line-clamp-2">{issue.name}</h5>
</a>
</Link>
<div
className={`flex items-center gap-2 text-xs ${
isDropdownActive ? "" : "overflow-x-scroll"
@@ -12,7 +12,6 @@ import issuesService from "services/issues.service";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
// components
import { SingleCalendarDate, CalendarHeader } from "components/core";
import { IssuePeekOverview } from "components/issues";
// ui
import { Spinner } from "components/ui";
// helpers
@@ -61,8 +60,7 @@ export const CalendarView: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } =
useCalendarIssuesView();
const { calendarIssues, params, setCalendarDateRange } = useCalendarIssuesView();
const totalDate = eachDayOfInterval({
start: calendarDates.startDate,
@@ -154,103 +152,90 @@ export const CalendarView: React.FC<Props> = ({
endDate,
});
setDisplayFilters({
calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(
endDate
)};before`,
});
setCalendarDateRange(
`${renderDateFormat(startDate)};after,${renderDateFormat(endDate)};before`
);
};
useEffect(() => {
if (!displayFilters || displayFilters.calendar_date_range === "")
setDisplayFilters({
calendar_date_range: `${renderDateFormat(
startOfWeek(currentDate)
)};after,${renderDateFormat(lastDayOfWeek(currentDate))};before`,
});
}, [currentDate, displayFilters, setDisplayFilters]);
setCalendarDateRange(
`${renderDateFormat(startOfWeek(currentDate))};after,${renderDateFormat(
lastDayOfWeek(currentDate)
)};before`
);
}, [currentDate]);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
return (
<>
<IssuePeekOverview
handleMutation={() => mutateIssues()}
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
{calendarIssues ? (
<div className="h-full overflow-y-auto">
<DragDropContext onDragEnd={onDragEnd}>
<div className="h-full rounded-lg p-8 text-custom-text-200">
<CalendarHeader
isMonthlyView={isMonthlyView}
setIsMonthlyView={setIsMonthlyView}
showWeekEnds={showWeekEnds}
setShowWeekEnds={setShowWeekEnds}
currentDate={currentDate}
setCurrentDate={setCurrentDate}
changeDateRange={changeDateRange}
/>
return calendarIssues ? (
<div className="h-full overflow-y-auto">
<DragDropContext onDragEnd={onDragEnd}>
<div className="h-full rounded-lg p-8 text-custom-text-200">
<CalendarHeader
isMonthlyView={isMonthlyView}
setIsMonthlyView={setIsMonthlyView}
showWeekEnds={showWeekEnds}
setShowWeekEnds={setShowWeekEnds}
currentDate={currentDate}
setCurrentDate={setCurrentDate}
changeDateRange={changeDateRange}
/>
<div
className={`grid auto-rows-[minmax(36px,1fr)] rounded-lg ${
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
}`}
>
{weeks.map((date, index) => (
<div
className={`grid auto-rows-[minmax(36px,1fr)] rounded-lg ${
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
key={index}
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
? showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
? ""
: "border-r"
: ""
}`}
>
{weeks.map((date, index) => (
<div
key={index}
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
? showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
? ""
: "border-r"
: ""
}`}
>
<span>
{isMonthlyView
? formatDate(date, "eee").substring(0, 3)
: formatDate(date, "eee")}
</span>
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
</div>
))}
<span>
{isMonthlyView
? formatDate(date, "eee").substring(0, 3)
: formatDate(date, "eee")}
</span>
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
</div>
))}
</div>
<div
className={`grid h-full ${isMonthlyView ? "auto-rows-min" : ""} ${
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
} `}
>
{currentViewDaysData.map((date, index) => (
<SingleCalendarDate
key={`${date}-${index}`}
index={index}
date={date}
handleIssueAction={handleIssueAction}
addIssueToDate={addIssueToDate}
isMonthlyView={isMonthlyView}
showWeekEnds={showWeekEnds}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
</div>
</DragDropContext>
<div
className={`grid h-full ${isMonthlyView ? "auto-rows-min" : ""} ${
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
} `}
>
{currentViewDaysData.map((date, index) => (
<SingleCalendarDate
key={`${date}-${index}`}
index={index}
date={date}
handleIssueAction={handleIssueAction}
addIssueToDate={addIssueToDate}
isMonthlyView={isMonthlyView}
showWeekEnds={showWeekEnds}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</>
</DragDropContext>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
);
};
@@ -1,5 +1,6 @@
import React, { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
@@ -157,15 +158,6 @@ export const SingleCalendarIssue: React.FC<Props> = ({
? Object.values(properties).some((value) => value === true)
: false;
const openPeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
return (
<div
key={index}
@@ -201,27 +193,23 @@ export const SingleCalendarIssue: React.FC<Props> = ({
</CustomMenu>
</div>
)}
<button
type="button"
className="flex w-full cursor-pointer flex-col items-start justify-center gap-1.5"
onClick={openPeekOverview}
>
{properties.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a className="flex w-full cursor-pointer flex-col items-start justify-center gap-1.5">
{properties.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
)}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-xs text-custom-text-100">{truncateText(issue.name, 25)}</span>
</Tooltip>
)}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-xs text-custom-text-100">{truncateText(issue.name, 25)}</span>
</Tooltip>
</button>
</a>
</Link>
{displayProperties && (
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
{properties.priority && (
@@ -6,24 +6,20 @@ import { IssueGanttChartView } from "components/issues";
import { ModuleIssuesGanttChartView } from "components/modules";
import { ViewIssuesGanttChartView } from "components/views";
type Props = {
disableUserActions: boolean;
};
export const GanttChartView: React.FC<Props> = ({ disableUserActions }) => {
export const GanttChartView = () => {
const router = useRouter();
const { cycleId, moduleId, viewId } = router.query;
return (
<>
{cycleId ? (
<CycleIssuesGanttChartView disableUserActions={disableUserActions} />
<CycleIssuesGanttChartView />
) : moduleId ? (
<ModuleIssuesGanttChartView disableUserActions={disableUserActions} />
<ModuleIssuesGanttChartView />
) : viewId ? (
<ViewIssuesGanttChartView disableUserActions={disableUserActions} />
<ViewIssuesGanttChartView />
) : (
<IssueGanttChartView disableUserActions={disableUserActions} />
<IssueGanttChartView />
)}
</>
);
+51 -92
View File
@@ -19,12 +19,7 @@ import useIssuesProperties from "hooks/use-issue-properties";
import useProjectMembers from "hooks/use-project-members";
// components
import { FiltersList, AllViews } from "components/core";
import {
CreateUpdateIssueModal,
DeleteIssueModal,
DeleteDraftIssueModal,
CreateUpdateDraftIssueModal,
} from "components/issues";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateViewModal } from "components/views";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
@@ -75,20 +70,25 @@ export const IssuesView: React.FC<Props> = ({
// trash box
const [trashBox, setTrashBox] = useState(false);
// selected draft issue
const [selectedDraftIssue, setSelectedDraftIssue] = useState<IIssue | null>(null);
const [selectedDraftForDelete, setSelectDraftForDelete] = useState<IIssue | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const isDraftIssues = router.asPath.includes("draft-issues");
const { user } = useUserAuth();
const { setToastAlert } = useToast();
const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params } =
useIssuesView();
const {
groupedByIssues,
mutateIssues,
issueView,
groupByProperty: selectedGroup,
orderBy,
filters,
isEmpty,
setFilters,
params,
showEmptyGroups,
} = useIssuesView();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { data: stateGroups } = useSWR(
@@ -116,9 +116,6 @@ export const IssuesView: React.FC<Props> = ({
[setDeleteIssueModal, setIssueToDelete]
);
const handleDraftIssueClick = useCallback((issue: any) => setSelectedDraftIssue(issue), []);
const handleDraftIssueDelete = useCallback((issue: any) => setSelectDraftForDelete(issue), []);
const handleOnDragEnd = useCallback(
async (result: DropResult) => {
setTrashBox(false);
@@ -132,7 +129,7 @@ export const IssuesView: React.FC<Props> = ({
if (destination.droppableId === "trashBox") {
handleDeleteIssue(draggedItem);
} else {
if (displayFilters.order_by === "sort_order") {
if (orderBy === "sort_order") {
let newSortOrder = draggedItem.sort_order;
const destinationGroupArray = groupedByIssues[destination.droppableId];
@@ -180,19 +177,16 @@ export const IssuesView: React.FC<Props> = ({
const destinationGroup = destination.droppableId; // destination group id
if (
displayFilters.order_by === "sort_order" ||
source.droppableId !== destination.droppableId
) {
if (orderBy === "sort_order" || source.droppableId !== destination.droppableId) {
// different group/column;
// source.droppableId !== destination.droppableId -> even if order by is not sort_order,
// if the issue is moved to a different group, then we will change the group of the
// dragged item(or issue)
if (displayFilters.group_by === "priority")
if (selectedGroup === "priority")
draggedItem.priority = destinationGroup as TIssuePriorities;
else if (displayFilters.group_by === "state") {
else if (selectedGroup === "state") {
draggedItem.state = destinationGroup;
draggedItem.state_detail = states?.find((s) => s.id === destinationGroup) as IState;
}
@@ -219,14 +213,8 @@ export const IssuesView: React.FC<Props> = ({
return {
...prevData,
[sourceGroup]: orderArrayBy(
sourceGroupArray,
displayFilters.order_by ?? "-created_at"
),
[destinationGroup]: orderArrayBy(
destinationGroupArray,
displayFilters.order_by ?? "-created_at"
),
[sourceGroup]: orderArrayBy(sourceGroupArray, orderBy),
[destinationGroup]: orderArrayBy(destinationGroupArray, orderBy),
};
},
false
@@ -279,13 +267,13 @@ export const IssuesView: React.FC<Props> = ({
}
},
[
displayFilters.group_by,
displayFilters.order_by,
workspaceSlug,
cycleId,
moduleId,
groupedByIssues,
projectId,
selectedGroup,
orderBy,
handleDeleteIssue,
params,
states,
@@ -299,19 +287,19 @@ export const IssuesView: React.FC<Props> = ({
let preloadedValue: string | string[] = groupTitle;
if (displayFilters.group_by === "labels") {
if (selectedGroup === "labels") {
if (groupTitle === "None") preloadedValue = [];
else preloadedValue = [groupTitle];
}
if (displayFilters.group_by)
if (selectedGroup)
setPreloadedData({
[displayFilters.group_by]: preloadedValue,
[selectedGroup]: preloadedValue,
actionType: "createIssue",
});
else setPreloadedData({ actionType: "createIssue" });
},
[displayFilters.group_by, setCreateIssueModal, setPreloadedData]
[setCreateIssueModal, setPreloadedData, selectedGroup]
);
const addIssueToDate = useCallback(
@@ -356,14 +344,6 @@ export const IssuesView: React.FC<Props> = ({
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
);
const handleDraftIssueAction = useCallback(
(issue: IIssue, action: "edit" | "delete") => {
if (action === "edit") handleDraftIssueClick(issue);
else if (action === "delete") handleDraftIssueDelete(issue);
},
[handleDraftIssueClick, handleDraftIssueDelete]
);
const removeIssueFromCycle = useCallback(
(bridgeId: string, issueId: string) => {
if (!workspaceSlug || !projectId || !cycleId) return;
@@ -372,7 +352,7 @@ export const IssuesView: React.FC<Props> = ({
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (displayFilters.group_by) {
if (selectedGroup) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
@@ -404,7 +384,7 @@ export const IssuesView: React.FC<Props> = ({
console.log(e);
});
},
[displayFilters.group_by, workspaceSlug, projectId, cycleId, params, setToastAlert]
[workspaceSlug, projectId, cycleId, params, selectedGroup, setToastAlert]
);
const removeIssueFromModule = useCallback(
@@ -415,7 +395,7 @@ export const IssuesView: React.FC<Props> = ({
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (displayFilters.group_by) {
if (selectedGroup) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
@@ -447,7 +427,7 @@ export const IssuesView: React.FC<Props> = ({
console.log(e);
});
},
[displayFilters.group_by, workspaceSlug, projectId, moduleId, params, setToastAlert]
[workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
);
const nullFilters = Object.keys(filters).filter(
@@ -472,19 +452,6 @@ export const IssuesView: React.FC<Props> = ({
...preloadedData,
}}
/>
<CreateUpdateDraftIssueModal
isOpen={selectedDraftIssue !== null}
handleClose={() => setSelectedDraftIssue(null)}
data={
selectedDraftIssue
? {
...selectedDraftIssue,
is_draft: true,
}
: null
}
fieldsToShow={["all"]}
/>
<CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
handleClose={() => setEditIssueModal(false)}
@@ -496,12 +463,6 @@ export const IssuesView: React.FC<Props> = ({
data={issueToDelete}
user={user}
/>
<DeleteDraftIssueModal
data={selectedDraftForDelete}
isOpen={selectedDraftForDelete !== null}
handleClose={() => setSelectDraftForDelete(null)}
/>
{areFiltersApplied && (
<>
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
@@ -520,6 +481,7 @@ export const IssuesView: React.FC<Props> = ({
state: null,
start_date: null,
target_date: null,
type: null,
})
}
/>
@@ -551,34 +513,29 @@ export const IssuesView: React.FC<Props> = ({
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
dragDisabled={
displayFilters.group_by === "created_by" ||
displayFilters.group_by === "labels" ||
displayFilters.group_by === "state_detail.group" ||
displayFilters.group_by === "assignees"
selectedGroup === "created_by" ||
selectedGroup === "labels" ||
selectedGroup === "state_detail.group" ||
selectedGroup === "assignees"
}
emptyState={{
title: isDraftIssues
? "Draft issues will appear here"
: cycleId
title: cycleId
? "Cycle issues will appear here"
: moduleId
? "Module issues will appear here"
: "Project issues will appear here",
description: isDraftIssues
? "Draft issues are issues that are not yet created."
: "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.",
primaryButton: !isDraftIssues
? {
icon: <PlusIcon className="h-4 w-4" />,
text: "New Issue",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}
: undefined,
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.",
primaryButton: {
icon: <PlusIcon className="h-4 w-4" />,
text: "New Issue",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
},
secondaryButton:
cycleId || moduleId ? (
<SecondaryButton
@@ -592,18 +549,20 @@ export const IssuesView: React.FC<Props> = ({
}}
handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
openIssuesListModal={openIssuesListModal ?? null}
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
trashBox={trashBox}
setTrashBox={setTrashBox}
viewProps={{
groupByProperty: selectedGroup,
groupedIssues: groupedByIssues,
displayFilters,
isEmpty,
issueView,
mutateIssues,
orderBy,
params,
properties,
showEmptyGroups,
}}
/>
</>
@@ -1,12 +1,5 @@
import { useRouter } from "next/router";
// hooks
import useMyIssues from "hooks/my-issues/use-my-issues";
import useIssuesView from "hooks/use-issues-view";
import useProfileIssues from "hooks/use-profile-issues";
// components
import { SingleList } from "components/core/views/list-view/single-list";
import { IssuePeekOverview } from "components/issues";
// types
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
@@ -15,10 +8,7 @@ type Props = {
states: IState[] | undefined;
addIssueToGroup: (groupTitle: string) => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
openIssuesListModal?: (() => void) | null;
myIssueProjectId?: string | null;
handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
disableUserActions: boolean;
disableAddIssueOption?: boolean;
@@ -33,50 +23,23 @@ export const AllLists: React.FC<Props> = ({
disableUserActions,
disableAddIssueOption = false,
openIssuesListModal,
handleMyIssueOpen,
myIssueProjectId,
removeIssue,
states,
handleDraftIssueAction,
user,
userAuth,
viewProps,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, userId } = router.query;
const isProfileIssue =
router.pathname.includes("assigned") ||
router.pathname.includes("created") ||
router.pathname.includes("subscribed");
const isMyIssue = router.pathname.includes("my-issues");
const { mutateIssues } = useIssuesView();
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { mutateProfileIssues } = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
const { displayFilters, groupedIssues } = viewProps;
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
return (
<>
<IssuePeekOverview
handleMutation={() =>
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
}
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
{groupedIssues && (
<div className="h-full overflow-y-auto">
{Object.keys(groupedIssues).map((singleGroup) => {
const currentState =
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
return null;
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
return (
<SingleList
@@ -84,9 +47,7 @@ export const AllLists: React.FC<Props> = ({
groupTitle={singleGroup}
currentState={currentState}
addIssueToGroup={() => addIssueToGroup(singleGroup)}
handleDraftIssueAction={handleDraftIssueAction}
handleIssueAction={handleIssueAction}
handleMyIssueOpen={handleMyIssueOpen}
openIssuesListModal={openIssuesListModal}
removeIssue={removeIssue}
disableUserActions={disableUserActions}
@@ -1,5 +1,6 @@
import React, { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
@@ -17,7 +18,6 @@ import {
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
CreateUpdateDraftIssueModal,
} from "components/issues";
// ui
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
@@ -61,9 +61,6 @@ type Props = {
makeIssueCopy: () => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleDraftIssueSelect?: (issue: IIssue) => void;
handleDraftIssueDelete?: (issue: IIssue) => void;
handleMyIssueOpen?: (issue: IIssue) => void;
disableUserActions: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
@@ -78,14 +75,11 @@ export const SingleListIssue: React.FC<Props> = ({
makeIssueCopy,
removeIssue,
groupTitle,
handleDraftIssueDelete,
handleDeleteIssue,
handleMyIssueOpen,
disableUserActions,
user,
userAuth,
viewProps,
handleDraftIssueSelect,
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
@@ -94,11 +88,10 @@ export const SingleListIssue: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
const { setToastAlert } = useToast();
const { displayFilters, properties, mutateIssues } = viewProps;
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => {
@@ -131,9 +124,9 @@ export const SingleListIssue: React.FC<Props> = ({
handleIssuesMutation(
formData,
groupTitle ?? "",
displayFilters?.group_by ?? null,
selectedGroup,
index,
displayFilters?.order_by ?? "-created_at",
orderBy,
prevData
),
false
@@ -155,14 +148,15 @@ export const SingleListIssue: React.FC<Props> = ({
});
},
[
displayFilters,
workspaceSlug,
cycleId,
moduleId,
userId,
groupTitle,
index,
selectedGroup,
mutateIssues,
orderBy,
user,
]
);
@@ -183,20 +177,8 @@ export const SingleListIssue: React.FC<Props> = ({
const issuePath = isArchivedIssues
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
: isDraftIssues
? `#`
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
const openPeekOverview = (issue: IIssue) => {
const { query } = router;
if (handleMyIssueOpen) handleMyIssueOpen(issue);
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
const isNotAllowed =
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
@@ -210,45 +192,26 @@ export const SingleListIssue: React.FC<Props> = ({
>
{!isNotAllowed && (
<>
<ContextMenu.Item
Icon={PencilIcon}
onClick={() => {
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
else editIssue();
}}
>
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
Edit issue
</ContextMenu.Item>
{!isDraftIssues && (
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy...
</ContextMenu.Item>
)}
<ContextMenu.Item
Icon={TrashIcon}
onClick={() => {
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
else handleDeleteIssue(issue);
}}
>
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy...
</ContextMenu.Item>
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
Delete issue
</ContextMenu.Item>
</>
)}
{!isDraftIssues && (
<>
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
</ContextMenu.Item>
<a href={issuePath} target="_blank" rel="noreferrer noopener">
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item>
</a>
</>
)}
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
</ContextMenu.Item>
<a href={issuePath} target="_blank" rel="noreferrer noopener">
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item>
</a>
</ContextMenu>
<div
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
onContextMenu={(e) => {
@@ -258,30 +221,23 @@ export const SingleListIssue: React.FC<Props> = ({
}}
>
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
<div className="group relative flex items-center gap-2">
{properties.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
<Link href={issuePath}>
<a className="group relative flex items-center gap-2">
{properties.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
)}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="truncate text-[0.825rem] text-custom-text-100">{issue.name}</span>
</Tooltip>
)}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<button
type="button"
className="truncate text-[0.825rem] text-custom-text-100"
onClick={() => {
if (!isDraftIssues) openPeekOverview(issue);
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
}}
>
{issue.name}
</button>
</Tooltip>
</div>
</a>
</Link>
</div>
<div
@@ -374,12 +330,7 @@ export const SingleListIssue: React.FC<Props> = ({
)}
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem
onClick={() => {
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
else editIssue();
}}
>
<CustomMenu.MenuItem onClick={editIssue}>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
@@ -393,25 +344,18 @@ export const SingleListIssue: React.FC<Props> = ({
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={() => {
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
else handleDeleteIssue(issue);
}}
>
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
{!isDraftIssues && (
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
@@ -40,9 +40,7 @@ type Props = {
groupTitle: string;
addIssueToGroup: () => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
openIssuesListModal?: (() => void) | null;
handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
disableUserActions: boolean;
disableAddIssueOption?: boolean;
@@ -57,8 +55,6 @@ export const SingleList: React.FC<Props> = ({
addIssueToGroup,
handleIssueAction,
openIssuesListModal,
handleDraftIssueAction,
handleMyIssueOpen,
removeIssue,
disableUserActions,
disableAddIssueOption = false,
@@ -73,7 +69,7 @@ export const SingleList: React.FC<Props> = ({
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const { displayFilters, groupedIssues } = viewProps;
const { groupByProperty: selectedGroup, groupedIssues } = viewProps;
const { data: issueLabels } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
@@ -94,7 +90,7 @@ export const SingleList: React.FC<Props> = ({
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
switch (displayFilters?.group_by) {
switch (selectedGroup) {
case "state":
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
@@ -117,7 +113,7 @@ export const SingleList: React.FC<Props> = ({
const getGroupIcon = () => {
let icon;
switch (displayFilters?.group_by) {
switch (selectedGroup) {
case "state":
icon = currentState && (
<StateGroupIcon
@@ -181,13 +177,13 @@ export const SingleList: React.FC<Props> = ({
<div className="flex items-center justify-between px-4 py-2.5 bg-custom-background-90">
<Disclosure.Button>
<div className="flex items-center gap-x-3">
{displayFilters?.group_by !== null && (
{selectedGroup !== null && (
<div className="flex items-center">{getGroupIcon()}</div>
)}
{displayFilters?.group_by !== null ? (
{selectedGroup !== null ? (
<h2
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
displayFilters?.group_by === "created_by" ? "" : "capitalize"
selectedGroup === "created_by" ? "" : "capitalize"
}`}
>
{getGroupTitle()}
@@ -255,17 +251,6 @@ export const SingleList: React.FC<Props> = ({
editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleDraftIssueSelect={
handleDraftIssueAction
? () => handleDraftIssueAction(issue, "edit")
: undefined
}
handleDraftIssueDelete={
handleDraftIssueAction
? () => handleDraftIssueAction(issue, "delete")
: undefined
}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => {
if (removeIssue !== null && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
@@ -22,10 +22,10 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
const { orderBy, setOrderBy } = useSpreadsheetIssuesView();
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
setDisplayFilters({ order_by: order });
setOrderBy(order);
setSelectedMenuItem(`${order}_${itemKey}`);
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
};
@@ -239,7 +239,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
</CustomMenu.MenuItem>
{selectedMenuItem &&
selectedMenuItem !== "" &&
displayFilters?.order_by !== "-created_at" &&
orderBy !== "-created_at" &&
selectedMenuItem.includes(col.propertyName) && (
<CustomMenu.MenuItem
className={`mt-0.5${
@@ -8,19 +8,15 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
// types
import { IIssue } from "types";
type Props = {
disableUserActions: boolean;
};
export const CycleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions }) => {
export const CycleIssuesGanttChartView = () => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const { displayFilters } = useIssuesView();
const { orderBy } = useIssuesView();
const { user } = useUser();
const { projectDetails } = useProjectDetails();
@@ -34,31 +30,23 @@ export const CycleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return (
<>
<IssuePeekOverview
handleMutation={() => mutateGanttIssues()}
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
<div className="w-full h-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) =>
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={orderBy === "sort_order" && isAllowed}
bottomSpacing
/>
<div className="w-full h-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) =>
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={displayFilters.order_by === "sort_order" && isAllowed}
bottomSpacing
/>
</div>
</>
</div>
);
};
+2 -11
View File
@@ -1,10 +1,8 @@
import React, { useEffect, useState, useRef } from "react";
import React, { useEffect, useState } from "react";
// headless ui
import { Tab, Transition, Popover } from "@headlessui/react";
// react colors
import { TwitterPicker } from "react-color";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// types
import { Props } from "./types";
// emojis
@@ -38,8 +36,6 @@ const EmojiIconPicker: React.FC<Props> = ({
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
const emojiPickerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setRecentEmojis(getRecentEmojis());
}, []);
@@ -48,8 +44,6 @@ const EmojiIconPicker: React.FC<Props> = ({
if (!value || value?.length === 0) onChange(getRandomEmoji());
}, [value, onChange]);
useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false));
return (
<Popover className="relative z-[1]">
<Popover.Button
@@ -69,10 +63,7 @@ const EmojiIconPicker: React.FC<Props> = ({
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-custom-border-200 bg-custom-background-80 shadow-lg">
<div
ref={emojiPickerRef}
className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-custom-border-200 bg-custom-background-80 p-2 shadow-xl"
>
<div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-custom-border-200 bg-custom-background-80 p-2 shadow-xl">
<Tab.Group as="div" className="flex h-full w-full flex-col">
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
{tabOptions.map((tab) => (
+1 -1
View File
@@ -66,7 +66,7 @@ export const SingleEstimate: React.FC<Props> = ({
return (
<>
<div className="gap-2 p-4 border-b border-custom-border-200">
<div className="gap-2 py-3">
<div className="flex items-center justify-between">
<div>
<h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">
+39 -50
View File
@@ -46,38 +46,32 @@ const IntegrationGuide = () => {
return (
<>
<div className="h-full w-full">
<div className="h-full space-y-2">
<>
<div>
<div className="space-y-2">
{EXPORTERS_LIST.map((service) => (
<div
key={service.provider}
className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6"
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"
>
<div className="flex items-start justify-between gap-4 w-full">
<div className="flex item-center gap-2.5">
<div className="relative h-10 w-10 flex-shrink-0">
<Image
src={service.logo}
layout="fill"
objectFit="cover"
alt={`${service.title} Logo`}
/>
</div>
<div>
<h3 className="flex items-center gap-4 text-sm font-medium">
{service.title}
</h3>
<p className="text-sm text-custom-text-200 tracking-tight">
{service.description}
</p>
</div>
<div className="flex items-center gap-4 whitespace-nowrap">
<div className="relative h-10 w-10 flex-shrink-0">
<Image
src={service.logo}
layout="fill"
objectFit="cover"
alt={`${service.title} Logo`}
/>
</div>
<div className="w-full">
<h3>{service.title}</h3>
<p className="text-sm text-custom-text-200">{service.description}</p>
</div>
<div className="flex-shrink-0">
<Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}>
<a>
<PrimaryButton>
<span className="capitalize">{service.type}</span>
<span className="capitalize">{service.type}</span> now
</PrimaryButton>
</a>
</Link>
@@ -86,11 +80,10 @@ const IntegrationGuide = () => {
</div>
))}
</div>
<div>
<div className="flex items-center justify-between pt-7 pb-3.5 border-b border-custom-border-200">
<div className="flex gap-2 items-center">
<h3 className="flex gap-2 text-xl font-medium">Previous Exports</h3>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4">
<h3 className="mb-2 flex gap-2 text-lg font-medium justify-between">
<div className="flex gap-2">
<div className="">Previous Exports</div>
<button
type="button"
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
@@ -135,31 +128,27 @@ const IntegrationGuide = () => {
<Icon iconName="keyboard_arrow_right" className="!text-lg" />
</button>
</div>
</div>
<div className="flex flex-col">
{exporterServices && exporterServices?.results ? (
exporterServices?.results?.length > 0 ? (
<div>
<div className="divide-y divide-custom-border-200">
{exporterServices?.results.map((service) => (
<SingleExport key={service.id} service={service} refreshing={refreshing} />
))}
</div>
</h3>
{exporterServices && exporterServices?.results ? (
exporterServices?.results?.length > 0 ? (
<div className="space-y-2">
<div className="divide-y divide-custom-border-200">
{exporterServices?.results.map((service) => (
<SingleExport key={service.id} service={service} refreshing={refreshing} />
))}
</div>
) : (
<p className="text-sm text-custom-text-200 px-4 py-6">
No previous export available.
</p>
)
</div>
) : (
<Loader className="mt-6 grid grid-cols-1 gap-3">
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
</Loader>
)}
</div>
<p className="py-2 text-sm text-custom-text-200">No previous export available.</p>
)
) : (
<Loader className="mt-6 grid grid-cols-1 gap-3">
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
</Loader>
)}
</div>
</>
{provider && (
+1 -1
View File
@@ -23,7 +23,7 @@ export const SingleExport: React.FC<Props> = ({ service, refreshing }) => {
};
return (
<div className="flex items-center justify-between gap-2 px-4 py-3">
<div className="flex items-center justify-between gap-2 py-3">
<div>
<h4 className="flex items-center gap-2 text-sm">
<span>
-2
View File
@@ -83,5 +83,3 @@ export * from "./archive-icon";
export * from "./clock-icon";
export * from "./bell-icon";
export * from "./single-comment-icon";
export * from "./related-icon";
export * from "./module-icon";
-59
View File
@@ -1,59 +0,0 @@
import React from "react";
import type { Props } from "./types";
export const ModuleIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
color = "#F15B5B",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z"
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.84925 4.66667H4.81221C4.73039 4.66667 4.66406 4.733 4.66406 4.81482V5.85185C4.66406 5.93367 4.73039 6 4.81221 6H5.84925C5.93107 6 5.9974 5.93367 5.9974 5.85185V4.81482C5.9974 4.733 5.93107 4.66667 5.84925 4.66667Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.84925 10H4.81221C4.73039 10 4.66406 10.0663 4.66406 10.1481V11.1852C4.66406 11.267 4.73039 11.3333 4.81221 11.3333H5.84925C5.93107 11.3333 5.9974 11.267 5.9974 11.1852V10.1481C5.9974 10.0663 5.93107 10 5.84925 10Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11.1852 4.66667H10.1481C10.0663 4.66667 10 4.733 10 4.81482V5.85185C10 5.93367 10.0663 6 10.1481 6H11.1852C11.267 6 11.3333 5.93367 11.3333 5.85185V4.81482C11.3333 4.733 11.267 4.66667 11.1852 4.66667Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11.1852 10H10.1481C10.0663 10 10 10.0663 10 10.1481V11.1852C10 11.267 10.0663 11.3333 10.1481 11.3333H11.1852C11.267 11.3333 11.3333 11.267 11.3333 11.1852V10.1481C11.3333 10.0663 11.267 10 11.1852 10Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
-41
View File
@@ -1,41 +0,0 @@
import React from "react";
import type { Props } from "./types";
export const RelatedIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "rgb(var(--color-text-200))",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20 13L20 6C20 5.46957 19.7893 4.96086 19.4142 4.58579C19.0391 4.21071 18.5304 4 18 4L4 4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6L2 20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22L11 22"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12.125 19.25L9 16.125L12.125 13"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M20 22V18.1818C20 17.6032 19.7366 17.0482 19.2678 16.639C18.7989 16.2299 18.163 16 17.5 16H10"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
@@ -66,8 +66,6 @@ export const SelectRepository: React.FC<Props> = ({
content: <p>{truncateText(repo.full_name, characterLimit)}</p>,
})) ?? [];
if (userRepositories.length < 1) return null;
return (
<CustomSearchSelect
value={value}
+80 -81
View File
@@ -21,6 +21,7 @@ import {
import { Loader, PrimaryButton } from "components/ui";
// icons
import { ArrowPathIcon } from "@heroicons/react/24/outline";
import { ArrowRightIcon } from "components/icons";
// types
import { IImporterService } from "types";
// fetch-keys
@@ -56,10 +57,10 @@ const IntegrationGuide = () => {
data={importToDelete}
user={user}
/>
<div className="h-full">
<div className="h-full space-y-2">
{(!provider || provider === "csv") && (
<>
{/* <div className="mb-5 flex items-center gap-2">
<div className="mb-5 flex items-center gap-2">
<div className="h-full w-full space-y-1">
<div className="text-lg font-medium">Relocation Guide</div>
<div className="text-sm">
@@ -77,87 +78,85 @@ const IntegrationGuide = () => {
<ArrowRightIcon width={"18px"} color={"#3F76FF"} />
</div>
</a>
</div> */}
{IMPORTERS_EXPORTERS_LIST.map((service) => (
<div
key={service.provider}
className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6"
>
<div className="flex items-start gap-4">
<div className="relative h-10 w-10 flex-shrink-0">
<Image
src={service.logo}
layout="fill"
objectFit="cover"
alt={`${service.title} Logo`}
/>
</div>
<div>
<h3 className="flex items-center gap-4 text-sm font-medium">{service.title}</h3>
<p className="text-sm text-custom-text-200 tracking-tight">
{service.description}
</p>
</div>
</div>
<div className="flex-shrink-0">
<Link href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}>
<a>
<PrimaryButton>
<span className="capitalize">{service.type}</span>
</PrimaryButton>
</a>
</Link>
</div>
</div>
))}
<div>
<div className="flex items-center pt-7 pb-3.5 border-b border-custom-border-200">
<h3 className="flex gap-2 text-xl font-medium">
Previous Imports
<button
type="button"
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
onClick={() => {
setRefreshing(true);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
setRefreshing(false)
);
}}
>
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
{refreshing ? "Refreshing..." : "Refresh status"}
</button>
</h3>
</div>
<div className="flex flex-col">
{importerServices ? (
importerServices.length > 0 ? (
<div className="space-y-2">
<div className="divide-y divide-custom-border-200">
{importerServices.map((service) => (
<SingleImport
key={service.id}
service={service}
refreshing={refreshing}
handleDelete={() => handleDeleteImport(service)}
/>
))}
</div>
</div>
<div className="space-y-2">
{IMPORTERS_EXPORTERS_LIST.map((service) => (
<div
key={service.provider}
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"
>
<div className="flex items-center gap-4 whitespace-nowrap">
<div className="relative h-10 w-10 flex-shrink-0">
<Image
src={service.logo}
layout="fill"
objectFit="cover"
alt={`${service.title} Logo`}
/>
</div>
) : (
<p className="text-sm text-custom-text-200 px-4 py-6">
No previous imports available.
</p>
)
<div className="w-full">
<h3>{service.title}</h3>
<p className="text-sm text-custom-text-200">{service.description}</p>
</div>
<div className="flex-shrink-0">
<Link
href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}
>
<a>
<PrimaryButton>
<span className="capitalize">{service.type}</span> now
</PrimaryButton>
</a>
</Link>
</div>
</div>
</div>
))}
</div>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4">
<h3 className="mb-2 flex gap-2 text-lg font-medium">
Previous Imports
<button
type="button"
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
onClick={() => {
setRefreshing(true);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
setRefreshing(false)
);
}}
>
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
{refreshing ? "Refreshing..." : "Refresh status"}
</button>
</h3>
{importerServices ? (
importerServices.length > 0 ? (
<div className="space-y-2">
<div className="divide-y divide-custom-border-200">
{importerServices.map((service) => (
<SingleImport
key={service.id}
service={service}
refreshing={refreshing}
handleDelete={() => handleDeleteImport(service)}
/>
))}
</div>
</div>
) : (
<Loader className="mt-6 grid grid-cols-1 gap-3">
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
</Loader>
)}
</div>
<p className="py-2 text-sm text-custom-text-200">
No previous imports available.
</p>
)
) : (
<Loader className="mt-6 grid grid-cols-1 gap-3">
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
</Loader>
)}
</div>
</>
)}
+1 -1
View File
@@ -16,7 +16,7 @@ type Props = {
};
export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => (
<div className="flex items-center justify-between gap-2 px-4 py-3">
<div className="flex items-center justify-between gap-2 py-3">
<div>
<h4 className="flex items-center gap-2 text-sm">
<span>
@@ -15,7 +15,6 @@ import { DangerButton, Loader, PrimaryButton } from "components/ui";
// icons
import GithubLogo from "public/services/github.png";
import SlackLogo from "public/services/slack.png";
import { CheckCircle2 } from "lucide-react";
// types
import { IAppIntegration, IWorkspaceIntegration } from "types";
// fetch-keys
@@ -28,12 +27,13 @@ type Props = {
const integrationDetails: { [key: string]: any } = {
github: {
logo: GithubLogo,
installed: "Activate GitHub on individual projects to sync with specific repositories.",
installed:
"Activate GitHub integrations on individual projects to sync with specific repositories.",
notInstalled: "Connect with GitHub with your Plane workspace to sync project issues.",
},
slack: {
logo: SlackLogo,
installed: "Activate Slack on individual projects to sync with specific channels.",
installed: "Activate Slack integrations on individual projects to sync with specific channels.",
notInstalled: "Connect with Slack with your Plane workspace to sync project issues.",
},
};
@@ -99,22 +99,31 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
);
return (
<div className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6">
<div className="flex items-center justify-between gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5">
<div className="flex items-start gap-4">
<div className="h-10 w-10 flex-shrink-0">
<div className="h-12 w-12 flex-shrink-0">
<Image
src={integrationDetails[integration.provider].logo}
alt={`${integration.title} Logo`}
/>
</div>
<div>
<h3 className="flex items-center gap-2 text-sm font-medium">
<h3 className="flex items-center gap-4 text-xl font-semibold">
{integration.title}
{workspaceIntegrations
? isInstalled && <CheckCircle2 className="h-3.5 w-3.5 text-white fill-green-500" />
: null}
{workspaceIntegrations ? (
isInstalled ? (
<span className="flex items-center gap-1 text-sm font-normal text-green-500">
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500" /> Installed
</span>
) : (
<span className="flex items-center gap-1 text-sm font-normal text-custom-text-200">
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-custom-background-80" />{" "}
Not Installed
</span>
)
) : null}
</h3>
<p className="text-sm text-custom-text-200 tracking-tight">
<p className="text-sm text-custom-text-200">
{workspaceIntegrations
? isInstalled
? integrationDetails[integration.provider].installed
@@ -126,12 +135,12 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
{workspaceIntegrations ? (
isInstalled ? (
<DangerButton onClick={handleRemoveIntegration} loading={deletingIntegration} outline>
{deletingIntegration ? "Uninstalling..." : "Uninstall"}
<DangerButton onClick={handleRemoveIntegration} loading={deletingIntegration}>
{deletingIntegration ? "Removing..." : "Remove installation"}
</DangerButton>
) : (
<PrimaryButton onClick={startAuth} loading={isInstalling}>
{isInstalling ? "Installing..." : "Install"}
{isInstalling ? "Installing..." : "Add installation"}
</PrimaryButton>
)
) : (
@@ -83,7 +83,9 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
{projectIntegration ? (
<button
type="button"
className={`relative inline-flex h-4 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none bg-gray-700`}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
slackChannelAvailabilityToggle ? "bg-green-500" : "bg-gray-200"
}`}
role="switch"
aria-checked
onClick={() => {
@@ -92,8 +94,8 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
>
<span
aria-hidden="true"
className={`self-center inline-block h-2 w-2 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
slackChannelAvailabilityToggle ? "translate-x-3" : "translate-x-0"
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
slackChannelAvailabilityToggle ? "translate-x-5" : "translate-x-0"
}`}
/>
</button>
@@ -1,93 +0,0 @@
import React, { useState } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// ui
import { SecondaryButton, PrimaryButton } from "components/ui";
type Props = {
isOpen: boolean;
handleClose: () => void;
onDiscard: () => void;
onConfirm: () => Promise<void>;
};
export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
const { isOpen, handleClose, onDiscard, onConfirm } = props;
const [isLoading, setIsLoading] = useState(false);
const onClose = () => {
handleClose();
setIsLoading(false);
};
const handleDeletion = async () => {
setIsLoading(true);
await onConfirm();
setIsLoading(false);
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<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-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
<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 overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Draft Issue
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Would you like to save this issue in drafts?
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-between gap-2 p-4 sm:px-6">
<div>
<SecondaryButton onClick={onDiscard}>Discard</SecondaryButton>
</div>
<div className="space-x-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<PrimaryButton onClick={handleDeletion} loading={isLoading}>
{isLoading ? "Saving..." : "Save Draft"}
</PrimaryButton>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
@@ -1,145 +0,0 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import issueServices from "services/issues.service";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useToast from "hooks/use-toast";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { SecondaryButton, DangerButton } from "components/ui";
// types
import type { IIssue, ICurrentUserResponse } from "types";
// fetch-keys
import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
handleClose: () => void;
data: IIssue | null;
user?: ICurrentUserResponse;
onSubmit?: () => Promise<void> | void;
};
export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
const { isOpen, handleClose, data, user, onSubmit } = props;
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { params } = useIssuesView();
const { setToastAlert } = useToast();
useEffect(() => {
setIsDeleteLoading(false);
}, [isOpen]);
const onClose = () => {
setIsDeleteLoading(false);
handleClose();
};
const handleDeletion = async () => {
if (!workspaceSlug || !data || !user) return;
setIsDeleteLoading(true);
await issueServices
.deleteDraftIssue(workspaceSlug as string, data.project, data.id, user)
.then(() => {
setIsDeleteLoading(false);
handleClose();
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
setToastAlert({
title: "Success",
message: "Draft Issue deleted successfully",
type: "success",
});
})
.catch((error) => {
console.log(error);
handleClose();
setToastAlert({
title: "Error",
message: "Something went wrong",
type: "error",
});
setIsDeleteLoading(false);
});
if (onSubmit) await onSubmit();
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" 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-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-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 overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</span>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Draft Issue</h3>
</span>
</div>
<span>
<p className="text-sm text-custom-text-200">
Are you sure you want to delete issue{" "}
<span className="break-words font-medium text-custom-text-100">
{data?.project_detail.identifier}-{data?.sequence_id}
</span>
{""}? All of the data related to the draft issue will be permanently removed.
This action cannot be undone.
</p>
</span>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
</DangerButton>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
+3 -3
View File
@@ -50,7 +50,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
const { workspaceSlug, projectId, cycleId, moduleId, viewId, issueId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const { displayFilters, params } = useIssuesView();
const { issueView, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
@@ -73,7 +73,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
await issueServices
.deleteIssue(workspaceSlug as string, data.project, data.id, user)
.then(() => {
if (displayFilters.layout === "calendar") {
if (issueView === "calendar") {
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
@@ -87,7 +87,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
(prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
false
);
} else if (displayFilters.layout === "spreadsheet") {
} else if (issueView === "spreadsheet") {
const spreadsheetFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
: moduleId
-580
View File
@@ -1,580 +0,0 @@
import React, { FC, useState, useEffect, useRef } from "react";
import { useRouter } from "next/router";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// services
import aiService from "services/ai.service";
// hooks
import useToast from "hooks/use-toast";
// components
import { GptAssistantModal } from "components/core";
import { ParentIssuesListModal } from "components/issues";
import {
IssueAssigneeSelect,
IssueDateSelect,
IssueEstimateSelect,
IssueLabelSelect,
IssuePrioritySelect,
IssueProjectSelect,
IssueStateSelect,
} from "components/issues/select";
import { CreateStateModal } from "components/states";
import { CreateLabelModal } from "components/labels";
// ui
import { CustomMenu, Input, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
import { TipTapEditor } from "components/tiptap";
// icons
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
const defaultValues: Partial<IIssue> = {
project: "",
name: "",
description: {
type: "doc",
content: [
{
type: "paragraph",
},
],
},
description_html: "<p></p>",
estimate_point: null,
state: "",
parent: null,
priority: "none",
assignees: [],
assignees_list: [],
labels: [],
labels_list: [],
start_date: null,
target_date: null,
};
interface IssueFormProps {
handleFormSubmit: (formData: Partial<IIssue>) => Promise<void>;
data?: Partial<IIssue> | null;
prePopulatedData?: Partial<IIssue> | null;
projectId: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleClose: () => void;
status: boolean;
user: ICurrentUserResponse | undefined;
fieldsToShow: (
| "project"
| "name"
| "description"
| "state"
| "priority"
| "assignee"
| "label"
| "startDate"
| "dueDate"
| "estimate"
| "parent"
| "all"
)[];
}
export const DraftIssueForm: FC<IssueFormProps> = (props) => {
const {
handleFormSubmit,
data,
prePopulatedData,
projectId,
setActiveProject,
createMore,
setCreateMore,
handleClose,
status,
user,
fieldsToShow,
} = props;
const [stateModal, setStateModal] = useState(false);
const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const editorRef = useRef<any>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
watch,
control,
getValues,
setValue,
setFocus,
} = useForm<IIssue>({
defaultValues: prePopulatedData ?? defaultValues,
reValidateMode: "onChange",
});
const issueName = watch("name");
const onClose = () => {
handleClose();
};
const handleCreateUpdateIssue = async (
formData: Partial<IIssue>,
action: "saveDraft" | "createToNewIssue" = "saveDraft"
) => {
await handleFormSubmit({
...formData,
is_draft: action === "saveDraft",
});
setGptAssistantModal(false);
reset({
...defaultValues,
project: projectId,
description: {
type: "doc",
content: [
{
type: "paragraph",
},
],
},
description_html: "<p></p>",
});
editorRef?.current?.clearEditor();
};
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return;
setValue("description", {});
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
editorRef.current?.setEditorValue(`${watch("description_html")}`);
};
const handleAutoGenerateDescription = async () => {
if (!workspaceSlug || !projectId) return;
setIAmFeelingLucky(true);
aiService
.createGptTask(
workspaceSlug as string,
projectId as string,
{
prompt: issueName,
task: "Generate a proper description for this issue.",
},
user
)
.then((res) => {
if (res.response === "")
setToastAlert({
type: "error",
title: "Error!",
message:
"Issue title isn't informative enough to generate the description. Please try with a different title.",
});
else handleAiAssistance(res.response_html);
})
.catch((err) => {
const error = err?.data?.error;
if (err.status === 429)
setToastAlert({
type: "error",
title: "Error!",
message:
error ||
"You have reached the maximum number of requests of 50 requests per month per user.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: error || "Some error occurred. Please try again.",
});
})
.finally(() => setIAmFeelingLucky(false));
};
useEffect(() => {
setFocus("name");
reset({
...defaultValues,
...(prePopulatedData ?? {}),
...(data ?? {}),
});
}, [setFocus, prePopulatedData, reset, data]);
// update projectId in form when projectId changes
useEffect(() => {
reset({
...getValues(),
project: projectId,
});
}, [getValues, projectId, reset]);
const startDate = watch("start_date");
const targetDate = watch("target_date");
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
return (
<>
{projectId && (
<>
<CreateStateModal
isOpen={stateModal}
handleClose={() => setStateModal(false)}
projectId={projectId}
user={user}
/>
<CreateLabelModal
isOpen={labelModal}
handleClose={() => setLabelModal(false)}
projectId={projectId}
user={user}
onSuccess={(response) => {
setValue("labels", [...watch("labels"), response.id]);
setValue("labels_list", [...watch("labels_list"), response.id]);
}}
/>
</>
)}
<form
onSubmit={handleSubmit((formData) => handleCreateUpdateIssue(formData, "createToNewIssue"))}
>
<div className="space-y-5">
<div className="flex items-center gap-x-2">
{(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && (
<Controller
control={control}
name="project"
render={({ field: { value, onChange } }) => (
<IssueProjectSelect
value={value}
onChange={(val: string) => {
onChange(val);
setActiveProject(val);
}}
/>
)}
/>
)}
<h3 className="text-xl font-semibold leading-6 text-custom-text-100">
{status ? "Update" : "Create"} Issue
</h3>
</div>
{watch("parent") &&
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
selectedParentIssue && (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: selectedParentIssue.state__color,
}}
/>
<span className="flex-shrink-0 text-custom-text-200">
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
</span>
<span className="truncate font-medium">
{selectedParentIssue.name.substring(0, 50)}
</span>
<XMarkIcon
className="h-3 w-3 cursor-pointer"
onClick={() => {
setValue("parent", null);
setSelectedParentIssue(null);
}}
/>
</div>
</div>
)}
<div className="space-y-3">
<div className="mt-2 space-y-3">
{(fieldsToShow.includes("all") || fieldsToShow.includes("name")) && (
<div>
<Input
id="name"
name="name"
className="resize-none text-xl"
placeholder="Title"
autoComplete="off"
error={errors.name}
register={register}
validations={{
required: "Title is required",
maxLength: {
value: 255,
message: "Title should be less than 255 characters",
},
}}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
<div className="relative">
<div className="flex justify-end">
{issueName && issueName !== "" && (
<button
type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
>
{iAmFeelingLucky ? (
"Generating response..."
) : (
<>
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
</>
)}
</button>
)}
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
>
<SparklesIcon className="h-4 w-4" />
AI
</button>
</div>
<Controller
name="description_html"
control={control}
render={({ field: { value, onChange } }) => {
if (!value && !watch("description_html")) return <></>;
return (
<TipTapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
customClassName="min-h-[150px]"
onChange={(description: Object, description_html: string) => {
onChange(description_html);
setValue("description", description);
}}
/>
);
}}
/>
<GptAssistantModal
isOpen={gptAssistantModal}
handleClose={() => {
setGptAssistantModal(false);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
inset="top-2 left-0"
content=""
htmlContent={watch("description_html")}
onResponse={(response) => {
handleAiAssistance(response);
}}
projectId={projectId}
/>
</div>
)}
<div className="flex flex-wrap items-center gap-2">
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
<Controller
control={control}
name="state"
render={({ field: { value, onChange } }) => (
<IssueStateSelect
setIsOpen={setStateModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
<Controller
control={control}
name="priority"
render={({ field: { value, onChange } }) => (
<IssuePrioritySelect value={value} onChange={onChange} />
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
<Controller
control={control}
name="assignees"
render={({ field: { value, onChange } }) => (
<IssueAssigneeSelect
projectId={projectId}
value={value}
onChange={onChange}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
<Controller
control={control}
name="labels"
render={({ field: { value, onChange } }) => (
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
<div>
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Start date"
maxDate={maxDate ?? undefined}
onChange={onChange}
value={value}
/>
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
<div>
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Due date"
minDate={minDate ?? undefined}
onChange={onChange}
value={value}
/>
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
<div>
<Controller
control={control}
name="estimate_point"
render={({ field: { value, onChange } }) => (
<IssueEstimateSelect value={value} onChange={onChange} />
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<Controller
control={control}
name="parent"
render={({ field: { onChange } }) => (
<ParentIssuesListModal
isOpen={parentIssueListModalOpen}
handleClose={() => setParentIssueListModalOpen(false)}
onChange={(issue) => {
onChange(issue.id);
setSelectedParentIssue(issue);
}}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<CustomMenu ellipsis>
{watch("parent") ? (
<>
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setParentIssueListModalOpen(true)}
>
Change parent issue
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setValue("parent", null)}
>
Remove parent issue
</CustomMenu.MenuItem>
</>
) : (
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setParentIssueListModalOpen(true)}
>
Select Parent Issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
</div>
</div>
</div>
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-200 px-5 pt-5">
<div
className="flex cursor-pointer items-center gap-1"
onClick={() => setCreateMore((prevData) => !prevData)}
>
<span className="text-xs">Create more</span>
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
</div>
<div className="flex items-center gap-2">
<SecondaryButton onClick={onClose}>Discard</SecondaryButton>
<SecondaryButton
loading={isSubmitting}
onClick={handleSubmit((formData) => handleCreateUpdateIssue(formData, "saveDraft"))}
>
{isSubmitting ? "Saving..." : "Save Draft"}
</SecondaryButton>
{data && (
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Add Issue"}
</PrimaryButton>
)}
</div>
</div>
</form>
</>
);
};
-293
View File
@@ -1,293 +0,0 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// hooks
import useUser from "hooks/use-user";
import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useLocalStorage from "hooks/use-local-storage";
import useProjects from "hooks/use-projects";
import useMyIssues from "hooks/my-issues/use-my-issues";
// components
import { DraftIssueForm } from "components/issues";
// types
import type { IIssue } from "types";
// fetch-keys
import {
PROJECT_ISSUES_DETAILS,
USER_ISSUE,
SUB_ISSUES,
PROJECT_ISSUES_LIST_WITH_PARAMS,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
VIEW_ISSUES,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys";
interface IssuesModalProps {
data?: IIssue | null;
handleClose: () => void;
isOpen: boolean;
isUpdatingSingleIssue?: boolean;
prePopulateData?: Partial<IIssue>;
fieldsToShow?: (
| "project"
| "name"
| "description"
| "state"
| "priority"
| "assignee"
| "label"
| "startDate"
| "dueDate"
| "estimate"
| "parent"
| "all"
)[];
onSubmit?: (data: Partial<IIssue>) => Promise<void> | void;
}
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
data,
handleClose,
isOpen,
isUpdatingSingleIssue = false,
prePopulateData,
fieldsToShow = ["all"],
onSubmit,
}) => {
// states
const [createMore, setCreateMore] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { ...viewGanttParams } = params;
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
const { user } = useUser();
const { projects } = useProjects();
const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {});
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { setToastAlert } = useToast();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
prePopulateData = {
...prePopulateData,
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
};
const onClose = () => {
handleClose();
setActiveProject(null);
};
useEffect(() => {
// if modal is closed, reset active project to null
// and return to avoid activeProject being set to some other project
if (!isOpen) {
setActiveProject(null);
return;
}
// if data is present, set active project to the project of the
// issue. This has more priority than the project in the url.
if (data && data.project) {
setActiveProject(data.project);
return;
}
if (prePopulateData && prePopulateData.project)
return setActiveProject(prePopulateData.project);
// if data is not present, set active project to the project
// in the url. This has the least priority.
if (projects && projects.length > 0 && !activeProject)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects, isOpen, prePopulateData]);
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId
? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams);
const spreadsheetFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
: viewId
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", spreadsheetParams);
const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString())
: viewId
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "");
const createIssue = async (payload: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject || !user) return;
await issuesService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
.then(async () => {
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart")
mutate(ganttFetchKey, {
start_target_date: true,
order_by: "sort_order",
});
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues();
setToastAlert({
type: "success",
title: "Success!",
message: "Issue created successfully.",
});
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be created. Please try again.",
});
});
if (!createMore) onClose();
};
const updateIssue = async (payload: Partial<IIssue>) => {
if (!user) return;
await issuesService
.updateDraftIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else {
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
}
if (!createMore) onClose();
setToastAlert({
type: "success",
title: "Success!",
message: "Issue updated successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be updated. Please try again.",
});
});
};
const handleFormSubmit = async (formData: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject) return;
const payload: Partial<IIssue> = {
...formData,
assignees_list: formData.assignees ?? [],
labels_list: formData.labels ?? [],
description: formData.description ?? "",
description_html: formData.description_html ?? "<p></p>",
};
if (!data) await createIssue(payload);
else await updateIssue(payload);
clearDraftIssueLocalStorage();
if (onSubmit) await onSubmit(payload);
};
if (!projects || projects.length === 0) return null;
return (
<>
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" 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-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<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 border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<DraftIssueForm
handleFormSubmit={handleFormSubmit}
prePopulatedData={prePopulateData}
data={data}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={onClose}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
user={user}
fieldsToShow={fieldsToShow}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</>
);
};
+24 -46
View File
@@ -23,7 +23,14 @@ import {
import { CreateStateModal } from "components/states";
import { CreateLabelModal } from "components/labels";
// ui
import { CustomMenu, Input, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
import {
CustomMenu,
Input,
Loader,
PrimaryButton,
SecondaryButton,
ToggleSwitch,
} from "components/ui";
import { TipTapEditor } from "components/tiptap";
// icons
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
@@ -45,7 +52,7 @@ const defaultValues: Partial<IIssue> = {
estimate_point: null,
state: "",
parent: null,
priority: "none",
priority: null,
assignees: [],
assignees_list: [],
labels: [],
@@ -61,10 +68,9 @@ export interface IssueFormProps {
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleDiscardClose: () => void;
handleClose: () => void;
status: boolean;
user: ICurrentUserResponse | undefined;
handleFormDirty: (payload: Partial<IIssue> | null) => void;
fieldsToShow: (
| "project"
| "name"
@@ -81,21 +87,18 @@ export interface IssueFormProps {
)[];
}
export const IssueForm: FC<IssueFormProps> = (props) => {
const {
handleFormSubmit,
initialData,
projectId,
setActiveProject,
createMore,
setCreateMore,
handleDiscardClose,
status,
user,
fieldsToShow,
handleFormDirty,
} = props;
export const IssueForm: FC<IssueFormProps> = ({
handleFormSubmit,
initialData,
projectId,
setActiveProject,
createMore,
setCreateMore,
handleClose,
status,
user,
fieldsToShow,
}) => {
const [stateModal, setStateModal] = useState(false);
const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
@@ -113,7 +116,7 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
const {
register,
formState: { errors, isSubmitting, isDirty },
formState: { errors, isSubmitting },
handleSubmit,
reset,
watch,
@@ -128,25 +131,6 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
const issueName = watch("name");
const payload: Partial<IIssue> = {
name: getValues("name"),
description: getValues("description"),
state: getValues("state"),
priority: getValues("priority"),
assignees: getValues("assignees"),
labels: getValues("labels"),
start_date: getValues("start_date"),
target_date: getValues("target_date"),
project: getValues("project"),
parent: getValues("parent"),
};
useEffect(() => {
if (isDirty) handleFormDirty(payload);
else handleFormDirty(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(payload), isDirty]);
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
await handleFormSubmit(formData);
@@ -566,13 +550,7 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
</div>
<div className="flex items-center gap-2">
<SecondaryButton
onClick={() => {
handleDiscardClose();
}}
>
Discard
</SecondaryButton>
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{status
? isSubmitting
+4 -20
View File
@@ -11,21 +11,13 @@ import { IIssue } from "types";
export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
const router = useRouter();
const openPeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: data.id },
});
};
const { workspaceSlug } = router.query;
return (
<div
className="flex items-center relative h-full w-full rounded cursor-pointer"
style={{ backgroundColor: data?.state_detail?.color }}
onClick={openPeekOverview}
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
>
<div className="absolute top-0 left-0 h-full w-full bg-custom-background-100/50" />
<Tooltip
@@ -51,22 +43,14 @@ export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
// rendering issues on gantt sidebar
export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const duration = findTotalDaysInRange(data?.start_date ?? "", data?.target_date ?? "", true);
const openPeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: data.id },
});
};
return (
<div
className="relative w-full flex items-center gap-2 h-full cursor-pointer"
onClick={openPeekOverview}
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
>
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
<div className="text-xs text-custom-text-300 flex-shrink-0">
+20 -32
View File
@@ -8,19 +8,15 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
// types
import { IIssue } from "types";
type Props = {
disableUserActions: boolean;
};
export const IssueGanttChartView: React.FC<Props> = ({ disableUserActions }) => {
export const IssueGanttChartView = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { displayFilters } = useIssuesView();
const { orderBy } = useIssuesView();
const { user } = useUser();
const { projectDetails } = useProjectDetails();
@@ -33,31 +29,23 @@ export const IssueGanttChartView: React.FC<Props> = ({ disableUserActions }) =>
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return (
<>
<IssuePeekOverview
handleMutation={() => mutateGanttIssues()}
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
<div className="w-full h-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) =>
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
}
BlockRender={IssueGanttBlock}
SidebarBlockRender={IssueGanttSidebarBlock}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={orderBy === "sort_order" && isAllowed}
bottomSpacing
/>
<div className="w-full h-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) =>
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
}
BlockRender={IssueGanttBlock}
SidebarBlockRender={IssueGanttSidebarBlock}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={displayFilters.order_by === "sort_order" && isAllowed}
bottomSpacing
/>
</div>
</>
</div>
);
};
-6
View File
@@ -16,9 +16,3 @@ export * from "./sub-issues-list";
export * from "./label";
export * from "./issue-reaction";
export * from "./peek-overview";
export * from "./confirm-issue-discard";
// draft issue
export * from "./draft-issue-form";
export * from "./draft-issue-modal";
export * from "./delete-draft-issue-modal";
+59 -135
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/router";
@@ -19,9 +19,8 @@ import useInboxView from "hooks/use-inbox-view";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useProjects from "hooks/use-projects";
import useMyIssues from "hooks/my-issues/use-my-issues";
import useLocalStorage from "hooks/use-local-storage";
// components
import { IssueForm, ConfirmIssueDiscard } from "components/issues";
import { IssueForm } from "components/issues";
// types
import type { IIssue } from "types";
// fetch-keys
@@ -36,7 +35,6 @@ import {
MODULE_DETAILS,
VIEW_ISSUES,
INBOX_ISSUES,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys";
// constants
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
@@ -75,16 +73,14 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}) => {
// states
const [createMore, setCreateMore] = useState(false);
const [formDirtyState, setFormDirtyState] = useState<any>(null);
const [showConfirmDiscard, setShowConfirmDiscard] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query;
const { displayFilters, params } = useIssuesView();
const { issueView, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { ...viewGanttParams } = params;
const { order_by, group_by, ...viewGanttParams } = params;
const { params: inboxParams } = useInboxView();
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
@@ -93,36 +89,20 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { setValue: setValueInLocalStorage, clearValue: clearLocalStorageValue } =
useLocalStorage<any>("draftedIssue", {});
const { setToastAlert } = useToast();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
prePopulateData = {
...prePopulateData,
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
};
const onClose = () => {
if (!showConfirmDiscard) handleClose();
if (formDirtyState === null) return setActiveProject(null);
const data = JSON.stringify(formDirtyState);
setValueInLocalStorage(data);
};
const onDiscardClose = () => {
if (formDirtyState !== null) {
setShowConfirmDiscard(true);
} else {
handleClose();
setActiveProject(null);
}
};
const handleFormDirty = (data: any) => {
setFormDirtyState(data);
};
const onClose = useCallback(() => {
handleClose();
setActiveProject(null);
}, [handleClose]);
useEffect(() => {
// if modal is closed, reset active project to null
@@ -267,13 +247,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (payload.module && payload.module !== "")
await addIssueToModule(res.id, payload.module);
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart")
if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "gantt_chart")
mutate(ganttFetchKey, {
start_target_date: true,
order_by: "sort_order",
});
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues();
setToastAlert({
@@ -295,58 +275,18 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
});
});
if (!createMore) onDiscardClose();
};
const createDraftIssue = async () => {
if (!workspaceSlug || !activeProject || !user) return;
const payload: Partial<IIssue> = {
...formDirtyState,
};
await issuesService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
.then(() => {
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
if (groupedIssues) mutateMyIssues();
setToastAlert({
type: "success",
title: "Success!",
message: "Draft Issue created successfully.",
});
handleClose();
setActiveProject(null);
setFormDirtyState(null);
setShowConfirmDiscard(false);
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be created. Please try again.",
});
});
if (!createMore) onClose();
};
const updateIssue = async (payload: Partial<IIssue>) => {
if (!user) return;
await issuesService
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else {
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
}
@@ -354,7 +294,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
if (!createMore) onDiscardClose();
if (!createMore) onClose();
setToastAlert({
type: "success",
@@ -391,65 +331,49 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (!projects || projects.length === 0) return null;
return (
<>
<ConfirmIssueDiscard
isOpen={showConfirmDiscard}
handleClose={() => setShowConfirmDiscard(false)}
onConfirm={createDraftIssue}
onDiscard={() => {
handleClose();
setActiveProject(null);
setFormDirtyState(null);
setShowConfirmDiscard(false);
clearLocalStorageValue();
}}
/>
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" 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-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" 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-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<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 border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<IssueForm
handleFormSubmit={handleFormSubmit}
initialData={data ?? prePopulateData}
createMore={createMore}
setCreateMore={setCreateMore}
handleDiscardClose={onDiscardClose}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
user={user}
fieldsToShow={fieldsToShow}
handleFormDirty={handleFormDirty}
/>
</Dialog.Panel>
</Transition.Child>
</div>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<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 border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<IssueForm
handleFormSubmit={handleFormSubmit}
initialData={data ?? prePopulateData}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={onClose}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
user={user}
fieldsToShow={fieldsToShow}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</>
</div>
</Dialog>
</Transition.Root>
);
};
@@ -37,8 +37,20 @@ export const MyIssuesViewOptions: React.FC = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } =
useMyIssuesFilters(workspaceSlug?.toString());
const {
issueView,
setIssueView,
groupBy,
setGroupBy,
orderBy,
setOrderBy,
showEmptyGroups,
setShowEmptyGroups,
properties,
setProperty,
filters,
setFilters,
} = useMyIssuesFilters(workspaceSlug?.toString());
const { isEstimateActive } = useEstimateOption();
@@ -56,11 +68,11 @@ export const MyIssuesViewOptions: React.FC = () => {
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
displayFilters?.layout === option.type
issueView === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
}`}
onClick={() => setDisplayFilters({ layout: option.type })}
onClick={() => setIssueView(option.type)}
>
<option.Icon
sx={{
@@ -127,89 +139,81 @@ export const MyIssuesViewOptions: React.FC = () => {
<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-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
displayFilters?.group_by === "project"
? "Project"
: GROUP_BY_OPTIONS.find(
(option) => option.key === displayFilters?.group_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (displayFilters?.layout === "kanban" && option.key === null)
return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
groupBy === "project"
? "Project"
: GROUP_BY_OPTIONS.find((option) => option.key === groupBy)
?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupBy(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find(
(option) => option.key === displayFilters?.order_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (
displayFilters?.group_by === "priority" &&
option.key === "priority"
)
return null;
if (option.key === "sort_order") return null;
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (groupBy === "priority" && option.key === "priority")
return null;
if (option.key === "sort_order") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</>
)}
</div>
</>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find(
(option) => option.key === displayFilters?.type
)?.name ?? "Select"
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
@@ -218,7 +222,7 @@ export const MyIssuesViewOptions: React.FC = () => {
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setDisplayFilters({
setFilters({
type: option.key,
})
}
@@ -230,24 +234,16 @@ export const MyIssuesViewOptions: React.FC = () => {
</div>
</div>
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters?.show_empty_groups,
})
}
/>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
</div>
</>
)}
</div>
</>
)}
</div>
<div className="space-y-2 py-3">
@@ -257,7 +253,7 @@ export const MyIssuesViewOptions: React.FC = () => {
if (key === "estimate" && !isEstimateActive) return null;
if (
displayFilters?.layout === "spreadsheet" &&
issueView === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
@@ -265,7 +261,7 @@ export const MyIssuesViewOptions: React.FC = () => {
return null;
if (
displayFilters?.layout !== "spreadsheet" &&
issueView !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;

Some files were not shown because too many files have changed in this diff Show More