forked from github/plane
chore: merge conflict for lucide icons resolved
This commit is contained in:
commit
f579712092
54
.env.example
54
.env.example
@ -1,38 +1,3 @@
|
|||||||
# 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
|
# Database Settings
|
||||||
PGUSER="plane"
|
PGUSER="plane"
|
||||||
PGPASSWORD="plane"
|
PGPASSWORD="plane"
|
||||||
@ -45,15 +10,6 @@ REDIS_HOST="plane-redis"
|
|||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
REDIS_URL="redis://${REDIS_HOST}: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 Settings
|
||||||
AWS_REGION=""
|
AWS_REGION=""
|
||||||
AWS_ACCESS_KEY_ID="access-key"
|
AWS_ACCESS_KEY_ID="access-key"
|
||||||
@ -69,9 +25,6 @@ OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
|||||||
OPENAI_API_KEY="sk-" # add your openai key here
|
OPENAI_API_KEY="sk-" # add your openai key here
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
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
|
# Settings related to Docker
|
||||||
DOCKERIZED=1
|
DOCKERIZED=1
|
||||||
# set to 1 If using the pre-configured minio setup
|
# set to 1 If using the pre-configured minio setup
|
||||||
@ -80,10 +33,3 @@ USE_MINIO=1
|
|||||||
# Nginx Configuration
|
# Nginx Configuration
|
||||||
NGINX_PORT=80
|
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
apiserver/.env.example
Normal file
60
apiserver/.env.example
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# 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"
|
@ -31,8 +31,6 @@ from .issue import (
|
|||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
IssuePropertySerializer,
|
IssuePropertySerializer,
|
||||||
BlockerIssueSerializer,
|
|
||||||
BlockedIssueSerializer,
|
|
||||||
IssueAssigneeSerializer,
|
IssueAssigneeSerializer,
|
||||||
LabelSerializer,
|
LabelSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
@ -45,6 +43,8 @@ from .issue import (
|
|||||||
IssueReactionSerializer,
|
IssueReactionSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueVoteSerializer,
|
IssueVoteSerializer,
|
||||||
|
IssueRelationSerializer,
|
||||||
|
RelatedIssueSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,12 +17,10 @@ from plane.db.models import (
|
|||||||
IssueActivity,
|
IssueActivity,
|
||||||
IssueComment,
|
IssueComment,
|
||||||
IssueProperty,
|
IssueProperty,
|
||||||
IssueBlocker,
|
|
||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
IssueLabel,
|
IssueLabel,
|
||||||
Label,
|
Label,
|
||||||
IssueBlocker,
|
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
Cycle,
|
Cycle,
|
||||||
Module,
|
Module,
|
||||||
@ -32,6 +30,7 @@ from plane.db.models import (
|
|||||||
IssueReaction,
|
IssueReaction,
|
||||||
CommentReaction,
|
CommentReaction,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
|
IssueRelation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -81,25 +80,12 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
required=False,
|
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(
|
labels_list = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
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:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@ -122,10 +108,8 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
blockers = validated_data.pop("blockers_list", None)
|
|
||||||
assignees = validated_data.pop("assignees_list", None)
|
assignees = validated_data.pop("assignees_list", None)
|
||||||
labels = validated_data.pop("labels_list", None)
|
labels = validated_data.pop("labels_list", None)
|
||||||
blocks = validated_data.pop("blocks_list", None)
|
|
||||||
|
|
||||||
project_id = self.context["project_id"]
|
project_id = self.context["project_id"]
|
||||||
workspace_id = self.context["workspace_id"]
|
workspace_id = self.context["workspace_id"]
|
||||||
@ -137,22 +121,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
created_by_id = issue.created_by_id
|
created_by_id = issue.created_by_id
|
||||||
updated_by_id = issue.updated_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):
|
if assignees is not None and len(assignees):
|
||||||
IssueAssignee.objects.bulk_create(
|
IssueAssignee.objects.bulk_create(
|
||||||
[
|
[
|
||||||
@ -196,29 +164,11 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
batch_size=10,
|
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
|
return issue
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
blockers = validated_data.pop("blockers_list", None)
|
|
||||||
assignees = validated_data.pop("assignees_list", None)
|
assignees = validated_data.pop("assignees_list", None)
|
||||||
labels = validated_data.pop("labels_list", None)
|
labels = validated_data.pop("labels_list", None)
|
||||||
blocks = validated_data.pop("blocks_list", None)
|
|
||||||
|
|
||||||
# Related models
|
# Related models
|
||||||
project_id = instance.project_id
|
project_id = instance.project_id
|
||||||
@ -226,23 +176,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
created_by_id = instance.created_by_id
|
created_by_id = instance.created_by_id
|
||||||
updated_by_id = instance.updated_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:
|
if assignees is not None:
|
||||||
IssueAssignee.objects.filter(issue=instance).delete()
|
IssueAssignee.objects.filter(issue=instance).delete()
|
||||||
IssueAssignee.objects.bulk_create(
|
IssueAssignee.objects.bulk_create(
|
||||||
@ -277,23 +210,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
batch_size=10,
|
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
|
# Time updation occues even when other related models are updated
|
||||||
instance.updated_at = timezone.now()
|
instance.updated_at = timezone.now()
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
@ -375,32 +291,39 @@ class IssueLabelSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BlockedIssueSerializer(BaseSerializer):
|
class IssueRelationSerializer(BaseSerializer):
|
||||||
blocked_issue_detail = IssueProjectLiteSerializer(source="block", read_only=True)
|
related_issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueBlocker
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"blocked_issue_detail",
|
"related_issue_detail",
|
||||||
"blocked_by",
|
"relation_type",
|
||||||
"block",
|
"related_issue",
|
||||||
|
"issue",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
|
||||||
|
|
||||||
|
class RelatedIssueSerializer(BaseSerializer):
|
||||||
class BlockerIssueSerializer(BaseSerializer):
|
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
||||||
blocker_issue_detail = IssueProjectLiteSerializer(
|
|
||||||
source="blocked_by", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueBlocker
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"blocker_issue_detail",
|
"issue_detail",
|
||||||
"blocked_by",
|
"relation_type",
|
||||||
"block",
|
"related_issue",
|
||||||
|
"issue",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
|
||||||
|
|
||||||
|
|
||||||
class IssueAssigneeSerializer(BaseSerializer):
|
class IssueAssigneeSerializer(BaseSerializer):
|
||||||
@ -617,10 +540,8 @@ class IssueSerializer(BaseSerializer):
|
|||||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
||||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||||
# List of issues blocked by this issue
|
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
||||||
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
|
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
||||||
# List of issues that block this issue
|
|
||||||
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
|
|
||||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||||
|
@ -90,7 +90,9 @@ from plane.api.views import (
|
|||||||
IssueSubscriberViewSet,
|
IssueSubscriberViewSet,
|
||||||
IssueCommentPublicViewSet,
|
IssueCommentPublicViewSet,
|
||||||
IssueReactionViewSet,
|
IssueReactionViewSet,
|
||||||
|
IssueRelationViewSet,
|
||||||
CommentReactionViewSet,
|
CommentReactionViewSet,
|
||||||
|
IssueDraftViewSet,
|
||||||
## End Issues
|
## End Issues
|
||||||
# States
|
# States
|
||||||
StateViewSet,
|
StateViewSet,
|
||||||
@ -1010,6 +1012,47 @@ urlpatterns = [
|
|||||||
name="project-issue-archive",
|
name="project-issue-archive",
|
||||||
),
|
),
|
||||||
## End Issue Archives
|
## 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",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-draft",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
|
||||||
|
IssueDraftViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-draft",
|
||||||
|
),
|
||||||
|
## End Issue Drafts
|
||||||
## File Assets
|
## File Assets
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/file-assets/",
|
"workspaces/<str:slug>/file-assets/",
|
||||||
|
@ -86,8 +86,10 @@ from .issue import (
|
|||||||
IssueReactionPublicViewSet,
|
IssueReactionPublicViewSet,
|
||||||
CommentReactionPublicViewSet,
|
CommentReactionPublicViewSet,
|
||||||
IssueVotePublicViewSet,
|
IssueVotePublicViewSet,
|
||||||
|
IssueRelationViewSet,
|
||||||
IssueRetrievePublicEndpoint,
|
IssueRetrievePublicEndpoint,
|
||||||
ProjectIssuesPublicEndpoint,
|
ProjectIssuesPublicEndpoint,
|
||||||
|
IssueDraftViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .auth_extended import (
|
from .auth_extended import (
|
||||||
@ -167,6 +169,4 @@ from .analytic import (
|
|||||||
|
|
||||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||||
|
|
||||||
from .exporter import (
|
from .exporter import ExportIssuesEndpoint
|
||||||
ExportIssuesEndpoint,
|
|
||||||
)
|
|
@ -517,6 +517,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
try:
|
try:
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
order_by = request.GET.get("order_by", "created_at")
|
||||||
group_by = request.GET.get("group_by", False)
|
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")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||||
@ -555,9 +556,15 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
issues_data = IssueStateSerializer(issues, many=True).data
|
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:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues_data, group_by),
|
group_results(issues_data, group_by, sub_group_by),
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -51,6 +52,7 @@ from plane.api.serializers import (
|
|||||||
IssueReactionSerializer,
|
IssueReactionSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueVoteSerializer,
|
IssueVoteSerializer,
|
||||||
|
IssueRelationSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
@ -76,6 +78,7 @@ from plane.db.models import (
|
|||||||
CommentReaction,
|
CommentReaction,
|
||||||
ProjectDeployBoard,
|
ProjectDeployBoard,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
|
IssueRelation,
|
||||||
ProjectPublicMember,
|
ProjectPublicMember,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
@ -266,9 +269,16 @@ class IssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
## Grouping the results
|
## Grouping the results
|
||||||
group_by = request.GET.get("group_by", False)
|
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:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues, group_by), status=status.HTTP_200_OK
|
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
@ -443,9 +453,16 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
|
|
||||||
## Grouping the results
|
## Grouping the results
|
||||||
group_by = request.GET.get("group_by", False)
|
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:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues, group_by), status=status.HTTP_200_OK
|
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
@ -2040,6 +2057,98 @@ 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", [])
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
issueRelation = 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
IssueRelationSerializer(issueRelation, 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):
|
class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
@ -2240,3 +2349,157 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
|||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueDraftViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
serializer_class = IssueFlatSerializer
|
||||||
|
model = Issue
|
||||||
|
|
||||||
|
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 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
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -308,6 +308,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
try:
|
try:
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
order_by = request.GET.get("order_by", "created_at")
|
||||||
group_by = request.GET.get("group_by", False)
|
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")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||||
@ -346,9 +347,15 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
issues_data = IssueStateSerializer(issues, many=True).data
|
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:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues_data, group_by),
|
group_results(issues_data, group_by, sub_group_by),
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -220,7 +220,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
query = request.query_params.get("search", False)
|
query = request.query_params.get("search", False)
|
||||||
workspace_search = request.query_params.get("workspace_search", "false")
|
workspace_search = request.query_params.get("workspace_search", "false")
|
||||||
parent = request.query_params.get("parent", "false")
|
parent = request.query_params.get("parent", "false")
|
||||||
blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
|
issue_relation = request.query_params.get("issue_relation", "false")
|
||||||
cycle = request.query_params.get("cycle", "false")
|
cycle = request.query_params.get("cycle", "false")
|
||||||
module = request.query_params.get("module", "false")
|
module = request.query_params.get("module", "false")
|
||||||
sub_issue = request.query_params.get("sub_issue", "false")
|
sub_issue = request.query_params.get("sub_issue", "false")
|
||||||
@ -247,12 +247,12 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
"parent_id", flat=True
|
"parent_id", flat=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if blocker_blocked_by == "true" and issue_id:
|
if issue_relation == "true" and issue_id:
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
issues = issues.filter(
|
issues = issues.filter(
|
||||||
~Q(pk=issue_id),
|
~Q(pk=issue_id),
|
||||||
~Q(blocked_issues__block=issue),
|
~Q(issue_related__issue=issue),
|
||||||
~Q(blocker_issues__blocked_by=issue),
|
~Q(issue_relation__related_issue=issue),
|
||||||
)
|
)
|
||||||
if sub_issue == "true" and issue_id:
|
if sub_issue == "true" and issue_id:
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
|
@ -393,130 +393,6 @@ 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(
|
def create_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
):
|
):
|
||||||
@ -637,8 +513,6 @@ def update_issue_activity(
|
|||||||
"start_date": track_start_date,
|
"start_date": track_start_date,
|
||||||
"labels_list": track_labels,
|
"labels_list": track_labels,
|
||||||
"assignees_list": track_assignees,
|
"assignees_list": track_assignees,
|
||||||
"blocks_list": track_blocks,
|
|
||||||
"blockers_list": track_blockings,
|
|
||||||
"estimate_point": track_estimate_points,
|
"estimate_point": track_estimate_points,
|
||||||
"archived_at": track_archive_at,
|
"archived_at": track_archive_at,
|
||||||
"closed_to": track_closed_to,
|
"closed_to": track_closed_to,
|
||||||
@ -1170,6 +1044,57 @@ 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"):
|
||||||
|
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("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:
|
||||||
|
issue = Issue.objects.get(pk=current_instance.get("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 the {current_instance.get("relation_type")} relation',
|
||||||
|
old_identifier=current_instance.get("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Receive message from room group
|
# Receive message from room group
|
||||||
@shared_task
|
@shared_task
|
||||||
def issue_activity(
|
def issue_activity(
|
||||||
@ -1233,6 +1158,8 @@ def issue_activity(
|
|||||||
"link.activity.deleted": delete_link_activity,
|
"link.activity.deleted": delete_link_activity,
|
||||||
"attachment.activity.created": create_attachment_activity,
|
"attachment.activity.created": create_attachment_activity,
|
||||||
"attachment.activity.deleted": delete_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.created": create_issue_reaction_activity,
|
||||||
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
|
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
|
||||||
"comment_reaction.activity.created": create_comment_reaction_activity,
|
"comment_reaction.activity.created": create_comment_reaction_activity,
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
# 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),
|
||||||
|
]
|
138
apiserver/plane/db/migrations/0044_auto_20230913_0709.py
Normal file
138
apiserver/plane/db/migrations/0044_auto_20230913_0709.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# 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),
|
||||||
|
]
|
@ -32,6 +32,7 @@ from .issue import (
|
|||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
Label,
|
Label,
|
||||||
IssueBlocker,
|
IssueBlocker,
|
||||||
|
IssueRelation,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueSequence,
|
IssueSequence,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
@ -29,6 +29,7 @@ class IssueManager(models.Manager):
|
|||||||
| models.Q(issue_inbox__isnull=True)
|
| models.Q(issue_inbox__isnull=True)
|
||||||
)
|
)
|
||||||
.exclude(archived_at__isnull=False)
|
.exclude(archived_at__isnull=False)
|
||||||
|
.exclude(is_draft=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -83,6 +84,7 @@ class Issue(ProjectBaseModel):
|
|||||||
sort_order = models.FloatField(default=65535)
|
sort_order = models.FloatField(default=65535)
|
||||||
completed_at = models.DateTimeField(null=True)
|
completed_at = models.DateTimeField(null=True)
|
||||||
archived_at = models.DateField(null=True)
|
archived_at = models.DateField(null=True)
|
||||||
|
is_draft = models.BooleanField(default=False)
|
||||||
|
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
issue_objects = IssueManager()
|
issue_objects = IssueManager()
|
||||||
@ -178,6 +180,37 @@ class IssueBlocker(ProjectBaseModel):
|
|||||||
return f"{self.block.name} {self.blocked_by.name}"
|
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):
|
class IssueAssignee(ProjectBaseModel):
|
||||||
issue = models.ForeignKey(
|
issue = models.ForeignKey(
|
||||||
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
||||||
|
@ -25,13 +25,26 @@ ROLE_CHOICES = (
|
|||||||
|
|
||||||
def get_default_props():
|
def get_default_props():
|
||||||
return {
|
return {
|
||||||
"filters": {"type": None},
|
"filters": {
|
||||||
"orderBy": "-created_at",
|
"priority": None,
|
||||||
"collapsed": True,
|
"state": None,
|
||||||
"issueView": "list",
|
"state_group": None,
|
||||||
"filterIssue": None,
|
"assignees": None,
|
||||||
"groupByProperty": None,
|
"created_by": None,
|
||||||
"showEmptyGroups": True,
|
"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": "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,26 +16,41 @@ ROLE_CHOICES = (
|
|||||||
|
|
||||||
def get_default_props():
|
def get_default_props():
|
||||||
return {
|
return {
|
||||||
"filters": {"type": None},
|
"filters": {
|
||||||
"groupByProperty": None,
|
"priority": None,
|
||||||
"issueView": "list",
|
"state": None,
|
||||||
"orderBy": "-created_at",
|
"state_group": None,
|
||||||
"properties": {
|
"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": {
|
||||||
"assignee": True,
|
"assignee": True,
|
||||||
|
"attachment_count": True,
|
||||||
|
"created_on": True,
|
||||||
"due_date": True,
|
"due_date": True,
|
||||||
|
"estimate": True,
|
||||||
"key": True,
|
"key": True,
|
||||||
"labels": True,
|
"labels": True,
|
||||||
|
"link": True,
|
||||||
"priority": True,
|
"priority": True,
|
||||||
|
"start_date": True,
|
||||||
"state": True,
|
"state": True,
|
||||||
"sub_issue_count": True,
|
"sub_issue_count": True,
|
||||||
"attachment_count": True,
|
|
||||||
"link": True,
|
|
||||||
"estimate": True,
|
|
||||||
"created_on": True,
|
|
||||||
"updated_on": True,
|
"updated_on": True,
|
||||||
"start_date": True,
|
}
|
||||||
},
|
|
||||||
"showEmptyGroups": True,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ def resolve_keys(group_keys, value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def group_results(results_data, group_by):
|
def group_results(results_data, group_by, sub_group_by=False):
|
||||||
"""group results data into certain group_by
|
"""group results data into certain group_by
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -25,38 +25,64 @@ def group_results(results_data, group_by):
|
|||||||
Returns:
|
Returns:
|
||||||
obj: grouped results
|
obj: grouped results
|
||||||
"""
|
"""
|
||||||
response_dict = dict()
|
if sub_group_by:
|
||||||
|
main_responsive_dict = dict()
|
||||||
|
|
||||||
if group_by == "priority":
|
if sub_group_by == "priority":
|
||||||
response_dict = {
|
main_responsive_dict = {
|
||||||
"urgent": [],
|
"urgent": {},
|
||||||
"high": [],
|
"high": {},
|
||||||
"medium": [],
|
"medium": {},
|
||||||
"low": [],
|
"low": {},
|
||||||
"None": [],
|
"none": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
for value in results_data:
|
for value in results_data:
|
||||||
group_attribute = resolve_keys(group_by, value)
|
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||||
if isinstance(group_attribute, list):
|
if str(main_group_attribute) not in main_responsive_dict:
|
||||||
if len(group_attribute):
|
main_responsive_dict[str(main_group_attribute)] = {}
|
||||||
for attrib in group_attribute:
|
group_attribute = resolve_keys(group_by, value)
|
||||||
if str(attrib) in response_dict:
|
if str(group_attribute) in main_responsive_dict:
|
||||||
response_dict[str(attrib)].append(value)
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||||
else:
|
|
||||||
response_dict[str(attrib)] = []
|
|
||||||
response_dict[str(attrib)].append(value)
|
|
||||||
else:
|
else:
|
||||||
if str(None) in response_dict:
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
|
||||||
response_dict[str(None)].append(value)
|
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:
|
else:
|
||||||
response_dict[str(None)] = []
|
if str(None) in response_dict:
|
||||||
response_dict[str(None)].append(value)
|
response_dict[str(None)].append(value)
|
||||||
else:
|
else:
|
||||||
if str(group_attribute) in response_dict:
|
response_dict[str(None)] = []
|
||||||
response_dict[str(group_attribute)].append(value)
|
response_dict[str(None)].append(value)
|
||||||
else:
|
else:
|
||||||
response_dict[str(group_attribute)] = []
|
if str(group_attribute) in response_dict:
|
||||||
response_dict[str(group_attribute)].append(value)
|
response_dict[str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
response_dict[str(group_attribute)] = []
|
||||||
|
response_dict[str(group_attribute)].append(value)
|
||||||
|
|
||||||
return response_dict
|
return response_dict
|
||||||
|
@ -1,37 +1,5 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
x-api-and-worker-env: &api-and-worker-env
|
|
||||||
DEBUG: ${DEBUG}
|
|
||||||
SENTRY_DSN: ${SENTRY_DSN}
|
|
||||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
|
||||||
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
|
|
||||||
REDIS_URL: redis://plane-redis:6379/
|
|
||||||
EMAIL_HOST: ${EMAIL_HOST}
|
|
||||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
|
||||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
|
||||||
EMAIL_PORT: ${EMAIL_PORT}
|
|
||||||
EMAIL_FROM: ${EMAIL_FROM}
|
|
||||||
EMAIL_USE_TLS: ${EMAIL_USE_TLS}
|
|
||||||
EMAIL_USE_SSL: ${EMAIL_USE_SSL}
|
|
||||||
AWS_REGION: ${AWS_REGION}
|
|
||||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
|
||||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
|
|
||||||
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL}
|
|
||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT}
|
|
||||||
WEB_URL: ${WEB_URL}
|
|
||||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
|
|
||||||
DISABLE_COLLECTSTATIC: 1
|
|
||||||
DOCKERIZED: 1
|
|
||||||
OPENAI_API_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:
|
services:
|
||||||
plane-web:
|
plane-web:
|
||||||
container_name: planefrontend
|
container_name: planefrontend
|
||||||
@ -40,23 +8,8 @@ services:
|
|||||||
dockerfile: ./web/Dockerfile.web
|
dockerfile: ./web/Dockerfile.web
|
||||||
args:
|
args:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces
|
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh web/server.js web
|
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:
|
depends_on:
|
||||||
- plane-api
|
- plane-api
|
||||||
- plane-worker
|
- plane-worker
|
||||||
@ -68,14 +21,8 @@ services:
|
|||||||
dockerfile: ./space/Dockerfile.space
|
dockerfile: ./space/Dockerfile.space
|
||||||
args:
|
args:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh space/server.js space
|
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:
|
depends_on:
|
||||||
- plane-api
|
- plane-api
|
||||||
- plane-worker
|
- plane-worker
|
||||||
@ -91,9 +38,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: ./bin/takeoff
|
command: ./bin/takeoff
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
@ -108,9 +53,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: ./bin/worker
|
command: ./bin/worker
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- plane-api
|
||||||
- plane-db
|
- plane-db
|
||||||
@ -126,9 +69,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: ./bin/beat
|
command: ./bin/beat
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- plane-api
|
||||||
- plane-db
|
- plane-db
|
||||||
@ -163,8 +104,6 @@ services:
|
|||||||
command: server /export --console-address ":9090"
|
command: server /export --console-address ":9090"
|
||||||
volumes:
|
volumes:
|
||||||
- uploads:/export
|
- uploads:/export
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
environment:
|
||||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
||||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||||
@ -187,8 +126,6 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- ${NGINX_PORT}:80
|
- ${NGINX_PORT}:80
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
environment:
|
||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
|
@ -1,30 +1,29 @@
|
|||||||
events { }
|
events { }
|
||||||
|
|
||||||
|
|
||||||
http {
|
http {
|
||||||
sendfile on;
|
sendfile on;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
root /www/data/;
|
root /www/data/;
|
||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
|
|
||||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://planefrontend:3000/;
|
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 /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/;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -16,8 +16,12 @@
|
|||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.15",
|
||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
|
"postcss": "^8.4.29",
|
||||||
"prettier": "latest",
|
"prettier": "latest",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
"turbo": "latest"
|
"turbo": "latest"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.19"
|
"packageManager": "yarn@1.22.19"
|
||||||
|
@ -16,5 +16,7 @@ module.exports = {
|
|||||||
"no-duplicate-imports": "error",
|
"no-duplicate-imports": "error",
|
||||||
"arrow-body-style": ["error", "as-needed"],
|
"arrow-body-style": ["error", "as-needed"],
|
||||||
"react/self-closing-comp": ["error", { component: true, html: true }],
|
"react/self-closing-comp": ["error", { component: true, html: true }],
|
||||||
|
"@next/next/no-img-element": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
10
packages/tailwind-config-custom/package.json
Normal file
10
packages/tailwind-config-custom/package.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
7
packages/tailwind-config-custom/postcss.config.js
Normal file
7
packages/tailwind-config-custom/postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"tailwindcss/nesting": {},
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
212
packages/tailwind-config-custom/tailwind.config.js
Normal file
212
packages/tailwind-config-custom/tailwind.config.js
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
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")],
|
||||||
|
};
|
@ -17,6 +17,7 @@
|
|||||||
"next": "12.3.2",
|
"next": "12.3.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"tsconfig": "*",
|
"tsconfig": "*",
|
||||||
|
"tailwind-config-custom": "*",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
packages/ui/postcss.config.js
Normal file
1
packages/ui/postcss.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("tailwind-config-custom/postcss.config");
|
1
packages/ui/tailwind.config.js
Normal file
1
packages/ui/tailwind.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("tailwind-config-custom/tailwind.config");
|
@ -1,9 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig/nextjs.json",
|
"extends": "tsconfig/react-library.json",
|
||||||
"include": ["."],
|
"include": ["."],
|
||||||
"exclude": ["dist", "build", "node_modules"],
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"lib": ["DOM"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
#!/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" "{}"
|
|
17
setup.sh
17
setup.sh
@ -5,15 +5,12 @@ cp ./.env.example ./.env
|
|||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
export LC_CTYPE=C
|
export LC_CTYPE=C
|
||||||
|
|
||||||
|
cp ./web/.env.example ./web/.env
|
||||||
# Generate the NEXT_PUBLIC_API_BASE_URL with given IP
|
cp ./space/.env.example ./space/.env
|
||||||
echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env
|
cp ./apiserver/.env.example ./apiserver/.env
|
||||||
|
|
||||||
# Generate the SECRET_KEY that will be used by django
|
# Generate the SECRET_KEY that will be used by django
|
||||||
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.env
|
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||||
|
|
||||||
# WEB_URL for email redirection and image saving
|
|
||||||
echo -e "WEB_URL=$1" >> ./.env
|
|
||||||
|
|
||||||
# Generate Prompt for taking tiptap auth key
|
# Generate Prompt for taking tiptap auth key
|
||||||
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
|
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
|
||||||
@ -21,9 +18,7 @@ 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 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"
|
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/
|
echo "@tiptap-pro:registry=https://registry.tiptap.dev/
|
||||||
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
|
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
|
||||||
|
|
@ -1,8 +1,4 @@
|
|||||||
# 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
|
# Google Client ID for Google OAuth
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
||||||
# Flag to toggle OAuth
|
# Flag to toggle OAuth
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=1
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
@ -1,7 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: ["custom"],
|
extends: ["custom"],
|
||||||
rules: {
|
|
||||||
"@next/next/no-img-element": "off",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
FROM node:18-alpine AS builder
|
FROM node:18-alpine AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
|
||||||
|
|
||||||
RUN yarn global add turbo
|
RUN yarn global add turbo
|
||||||
COPY . .
|
COPY . .
|
||||||
@ -20,19 +19,16 @@ RUN yarn install --network-timeout 500000
|
|||||||
|
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
USER root
|
USER root
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||||
|
|
||||||
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
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
|
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||||
|
|
||||||
RUN yarn turbo run build --filter=space
|
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
|
FROM node:18-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -48,14 +44,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/.next ./space/.next
|
||||||
COPY --from=installer --chown=captain:plane /app/space/public ./space/public
|
COPY --from=installer --chown=captain:plane /app/space/public ./space/public
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||||
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
|
|
||||||
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
|
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
COPY start.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
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
USER captain
|
USER captain
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
// react hook form
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// components
|
// components
|
||||||
import { EmailResetPasswordForm } from "./email-reset-password-form";
|
import { EmailResetPasswordForm } from "./email-reset-password-form";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// components
|
// components
|
||||||
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
||||||
// images
|
// images
|
||||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
|
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||||
|
|
||||||
export const SignInView = observer(() => {
|
export const SignInView = observer(() => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
1
space/helpers/common.helper.ts
Normal file
1
space/helpers/common.helper.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
|
@ -17,7 +17,6 @@
|
|||||||
"@heroicons/react": "^2.0.12",
|
"@heroicons/react": "^2.0.12",
|
||||||
"@mui/icons-material": "^5.14.1",
|
"@mui/icons-material": "^5.14.1",
|
||||||
"@mui/material": "^5.14.1",
|
"@mui/material": "^5.14.1",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
|
||||||
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||||
"@tiptap/extension-color": "^2.0.4",
|
"@tiptap/extension-color": "^2.0.4",
|
||||||
@ -62,7 +61,6 @@
|
|||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
|
||||||
"@types/js-cookie": "^3.0.3",
|
"@types/js-cookie": "^3.0.3",
|
||||||
"@types/node": "18.14.1",
|
"@types/node": "18.14.1",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
@ -70,12 +68,10 @@
|
|||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||||
"autoprefixer": "^10.4.13",
|
|
||||||
"eslint": "8.34.0",
|
"eslint": "8.34.0",
|
||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"eslint-config-next": "13.2.1",
|
"eslint-config-next": "13.2.1",
|
||||||
"postcss": "^8.4.21",
|
|
||||||
"tsconfig": "*",
|
"tsconfig": "*",
|
||||||
"tailwindcss": "^3.2.7"
|
"tailwind-config-custom": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,16 @@ import React, { useEffect } from "react";
|
|||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
|
||||||
import authenticationService from "services/authentication.service";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// components
|
// components
|
||||||
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
||||||
|
|
||||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
|
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||||
|
|
||||||
const OnBoardingPage = () => {
|
const OnBoardingPage = () => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
|
||||||
const user = userStore?.currentUser;
|
const user = userStore?.currentUser;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const user = userStore?.currentUser;
|
const user = userStore?.currentUser;
|
||||||
|
|
||||||
|
@ -1,7 +1 @@
|
|||||||
module.exports = {
|
module.exports = require("tailwind-config-custom/postcss.config");
|
||||||
plugins: {
|
|
||||||
"tailwindcss/nesting": {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class AuthService extends APIService {
|
class AuthService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async emailLogin(data: any) {
|
async emailLogin(data: any) {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
// services
|
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
interface UnSplashImage {
|
interface UnSplashImage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -29,7 +27,7 @@ interface UnSplashImageUrls {
|
|||||||
|
|
||||||
class FileServices extends APIService {
|
class FileServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class IssueService extends APIService {
|
class IssueService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise<any> {
|
async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise<any> {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class ProjectService extends APIService {
|
class ProjectService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectSettings(workspace_slug: string, project_slug: string): Promise<any> {
|
async getProjectSettings(workspace_slug: string, project_slug: string): Promise<any> {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class UserService extends APIService {
|
class UserService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async currentUser(): Promise<any> {
|
async currentUser(): Promise<any> {
|
||||||
|
@ -1,203 +1 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
module.exports = require("tailwind-config-custom/tailwind.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
start.sh
4
start.sh
@ -1,9 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -x
|
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.."
|
echo "Starting Plane Frontend.."
|
||||||
node $1
|
node $1
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# Base url for the API requests
|
|
||||||
NEXT_PUBLIC_API_BASE_URL=""
|
|
||||||
# Extra image domains that need to be added for Next Image
|
# Extra image domains that need to be added for Next Image
|
||||||
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
||||||
# Google Client ID for Google OAuth
|
# Google Client ID for Google OAuth
|
||||||
@ -23,4 +21,4 @@ NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
|||||||
# For Telemetry, set it to "app.plane.so"
|
# For Telemetry, set it to "app.plane.so"
|
||||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
||||||
# Public boards deploy URL
|
# Public boards deploy URL
|
||||||
NEXT_PUBLIC_DEPLOY_URL=""
|
NEXT_PUBLIC_DEPLOY_URL="http://localhost:3000/spaces"
|
@ -1,7 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: ["custom"],
|
extends: ["custom"],
|
||||||
rules: {
|
|
||||||
"@next/next/no-img-element": "off",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,6 @@ FROM node:18-alpine AS builder
|
|||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
|
||||||
|
|
||||||
RUN yarn global add turbo
|
RUN yarn global add turbo
|
||||||
COPY . .
|
COPY . .
|
||||||
@ -14,8 +13,8 @@ FROM node:18-alpine AS installer
|
|||||||
|
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
ARG NEXT_PUBLIC_DEPLOY_URL=""
|
||||||
|
|
||||||
# First install the dependencies (as they change less often)
|
# First install the dependencies (as they change less often)
|
||||||
COPY .gitignore .gitignore
|
COPY .gitignore .gitignore
|
||||||
@ -26,18 +25,12 @@ RUN yarn install --network-timeout 500000
|
|||||||
# Build the project
|
# Build the project
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
USER root
|
USER root
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
|
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
||||||
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 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
|
FROM node:18-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -52,20 +45,15 @@ COPY --from=installer /app/web/package.json .
|
|||||||
# Automatically leverage output traces to reduce image size
|
# Automatically leverage output traces to reduce image size
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
# 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/standalone ./
|
||||||
|
|
||||||
COPY --from=installer --chown=captain:plane /app/web/.next ./web/.next
|
COPY --from=installer --chown=captain:plane /app/web/.next ./web/.next
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
ARG NEXT_PUBLIC_DEPLOY_URL=""
|
||||||
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
||||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
COPY start.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
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
USER captain
|
USER captain
|
||||||
|
@ -3,8 +3,8 @@ import React, { useState } from "react";
|
|||||||
// component
|
// component
|
||||||
import { CustomSelect, ToggleSwitch } from "components/ui";
|
import { CustomSelect, ToggleSwitch } from "components/ui";
|
||||||
import { SelectMonthModal } from "components/automation";
|
import { SelectMonthModal } from "components/automation";
|
||||||
// icons
|
// icon
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { ArchiveRestore } from "lucide-react";
|
||||||
// constants
|
// constants
|
||||||
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||||
// types
|
// types
|
||||||
@ -28,14 +28,18 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
|
|||||||
handleClose={() => setmonthModal(false)}
|
handleClose={() => setmonthModal(false)}
|
||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<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 flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
|
||||||
<div className="flex items-center justify-between gap-x-8 gap-y-2">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex items-start gap-3">
|
||||||
<h4 className="text-lg font-semibold">Auto-archive closed issues</h4>
|
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
|
||||||
<p className="text-sm text-custom-text-200">
|
<ArchiveRestore className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
|
||||||
Plane will automatically archive issues that have been completed or cancelled for the
|
</div>
|
||||||
configured time period.
|
<div className="">
|
||||||
</p>
|
<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>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={projectDetails?.archive_in !== 0}
|
value={projectDetails?.archive_in !== 0}
|
||||||
@ -47,40 +51,43 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{projectDetails?.archive_in !== 0 && (
|
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
|
||||||
<div className="w-1/2 text-base font-medium">
|
|
||||||
Auto-archive issues that are closed for
|
|
||||||
</div>
|
|
||||||
<div className="w-1/2">
|
|
||||||
<CustomSelect
|
|
||||||
value={projectDetails?.archive_in}
|
|
||||||
label={`${projectDetails?.archive_in} ${
|
|
||||||
projectDetails?.archive_in === 1 ? "Month" : "Months"
|
|
||||||
}`}
|
|
||||||
onChange={(val: number) => {
|
|
||||||
handleChange({ archive_in: val });
|
|
||||||
}}
|
|
||||||
input
|
|
||||||
verticalPosition="top"
|
|
||||||
width="w-full"
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
|
||||||
<CustomSelect.Option key={month.label} value={month.value}>
|
|
||||||
{month.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button
|
{projectDetails?.archive_in !== 0 && (
|
||||||
type="button"
|
<div className="ml-12">
|
||||||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
<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">
|
||||||
onClick={() => setmonthModal(true)}
|
<div className="w-1/2 text-sm font-medium">
|
||||||
>
|
Auto-archive issues that are closed for
|
||||||
Customize Time Range
|
</div>
|
||||||
</button>
|
<div className="w-1/2">
|
||||||
</>
|
<CustomSelect
|
||||||
</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>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -5,11 +5,12 @@ import useSWR from "swr";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// component
|
// component
|
||||||
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
|
import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui";
|
||||||
import { SelectMonthModal } from "components/automation";
|
import { SelectMonthModal } from "components/automation";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
import { StateGroupIcon } from "components/icons";
|
import { StateGroupIcon } from "components/icons";
|
||||||
|
import { ArchiveX } from "lucide-react";
|
||||||
// services
|
// services
|
||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
// constants
|
// constants
|
||||||
@ -76,14 +77,18 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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 flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
|
||||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 ">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex items-start gap-3">
|
||||||
<h4 className="text-lg font-semibold">Auto-close inactive issues</h4>
|
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
|
||||||
<p className="text-sm text-custom-text-200">
|
<ArchiveX className="h-4 w-4 text-red-500 flex-shrink-0" />
|
||||||
Plane will automatically close the issues that have not been updated for the
|
</div>
|
||||||
configured time period.
|
<div className="">
|
||||||
</p>
|
<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 haven’t been completed or canceled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={projectDetails?.close_in !== 0}
|
value={projectDetails?.close_in !== 0}
|
||||||
@ -95,82 +100,86 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{projectDetails?.close_in !== 0 && (
|
{projectDetails?.close_in !== 0 && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="ml-12">
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="w-1/2 text-base font-medium">
|
<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">
|
||||||
Auto-close issues that are inactive for
|
<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>
|
</div>
|
||||||
<div className="w-1/2">
|
|
||||||
<CustomSelect
|
<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">
|
||||||
value={projectDetails?.close_in}
|
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
|
||||||
label={`${projectDetails?.close_in} ${
|
<div className="w-1/2 ">
|
||||||
projectDetails?.close_in === 1 ? "Month" : "Months"
|
<CustomSearchSelect
|
||||||
}`}
|
value={
|
||||||
onChange={(val: number) => {
|
projectDetails?.default_state ? projectDetails?.default_state : defaultState
|
||||||
handleChange({ close_in: val });
|
}
|
||||||
}}
|
label={
|
||||||
input
|
<div className="flex items-center gap-2">
|
||||||
width="w-full"
|
{selectedOption ? (
|
||||||
>
|
<StateGroupIcon
|
||||||
<>
|
stateGroup={selectedOption.group}
|
||||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
color={selectedOption.color}
|
||||||
<CustomSelect.Option key={month.label} value={month.value}>
|
height="16px"
|
||||||
{month.label}
|
width="16px"
|
||||||
</CustomSelect.Option>
|
/>
|
||||||
))}
|
) : currentDefaultState ? (
|
||||||
<button
|
<StateGroupIcon
|
||||||
type="button"
|
stateGroup={currentDefaultState.group}
|
||||||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
color={currentDefaultState.color}
|
||||||
onClick={() => setmonthModal(true)}
|
height="16px"
|
||||||
>
|
width="16px"
|
||||||
Customize Time Range
|
/>
|
||||||
</button>
|
) : (
|
||||||
</>
|
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
</CustomSelect>
|
)}
|
||||||
</div>
|
{selectedOption?.name
|
||||||
</div>
|
? selectedOption.name
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
: currentDefaultState?.name ?? (
|
||||||
<div className="w-1/2 text-base font-medium">Auto-close Status</div>
|
<span className="text-custom-text-200">State</span>
|
||||||
<div className="w-1/2 ">
|
)}
|
||||||
<CustomSearchSelect
|
</div>
|
||||||
value={
|
}
|
||||||
projectDetails?.default_state ? projectDetails?.default_state : defaultState
|
onChange={(val: string) => {
|
||||||
}
|
handleChange({ default_state: val });
|
||||||
label={
|
}}
|
||||||
<div className="flex items-center gap-2">
|
options={options}
|
||||||
{selectedOption ? (
|
disabled={!multipleOptions}
|
||||||
<StateGroupIcon
|
width="w-full"
|
||||||
stateGroup={selectedOption.group}
|
input
|
||||||
color={selectedOption.color}
|
/>
|
||||||
height="16px"
|
</div>
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -104,7 +104,7 @@ export const SelectMonthModal: React.FC<Props> = ({
|
|||||||
as="h3"
|
as="h3"
|
||||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||||
>
|
>
|
||||||
Customize Time Range
|
Customise Time Range
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-8 flex items-center gap-2">
|
<div className="mt-8 flex items-center gap-2">
|
||||||
<div className="flex w-full flex-col gap-1 justify-center">
|
<div className="flex w-full flex-col gap-1 justify-center">
|
||||||
|
@ -20,6 +20,7 @@ import fileService from "services/file.service";
|
|||||||
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
|
||||||
const unsplashEnabled =
|
const unsplashEnabled =
|
||||||
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
|
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
|
||||||
@ -67,6 +68,8 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
fileService.getUnsplashImages(1, searchParams)
|
fileService.getUnsplashImages(1, searchParams)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const imagePickerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { workspaceDetails } = useWorkspaceDetails();
|
const { workspaceDetails } = useWorkspaceDetails();
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
@ -116,12 +119,14 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
onChange(images[0].urls.regular);
|
onChange(images[0].urls.regular);
|
||||||
}, [value, onChange, images]);
|
}, [value, onChange, images]);
|
||||||
|
|
||||||
|
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
|
||||||
|
|
||||||
if (!unsplashEnabled) return null;
|
if (!unsplashEnabled) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative z-[2]" ref={ref}>
|
<Popover className="relative z-[2]" ref={ref}>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="rounded-md border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
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"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@ -137,7 +142,10 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
||||||
<div className="h-96 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
|
||||||
|
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"
|
||||||
|
>
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<div>
|
<div>
|
||||||
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
@ -77,6 +77,8 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const [myIssueProjectId, setMyIssueProjectId] = useState<string | null>(null);
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { memberRole } = useProjectMyMembership();
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
@ -90,6 +92,10 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
const states = getStatesList(stateGroups);
|
const states = getStatesList(stateGroups);
|
||||||
|
|
||||||
|
const handleMyIssueOpen = (issue: IIssue) => {
|
||||||
|
setMyIssueProjectId(issue.project);
|
||||||
|
};
|
||||||
|
|
||||||
const handleTrashBox = useCallback(
|
const handleTrashBox = useCallback(
|
||||||
(isDragging: boolean) => {
|
(isDragging: boolean) => {
|
||||||
if (isDragging && !trashBox) setTrashBox(true);
|
if (isDragging && !trashBox) setTrashBox(true);
|
||||||
@ -128,6 +134,8 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
removeIssue={removeIssue}
|
removeIssue={removeIssue}
|
||||||
|
myIssueProjectId={myIssueProjectId}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
disableAddIssueOption={disableAddIssueOption}
|
disableAddIssueOption={disableAddIssueOption}
|
||||||
user={user}
|
user={user}
|
||||||
@ -143,6 +151,8 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
|
myIssueProjectId={myIssueProjectId}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={removeIssue}
|
removeIssue={removeIssue}
|
||||||
states={states}
|
states={states}
|
||||||
user={user}
|
user={user}
|
||||||
@ -166,7 +176,9 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
displayFilters?.layout === "gantt_chart" && <GanttChartView />
|
displayFilters?.layout === "gantt_chart" && (
|
||||||
|
<GanttChartView disableUserActions={disableUserActions} />
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : router.pathname.includes("archived-issues") ? (
|
) : router.pathname.includes("archived-issues") ? (
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
|
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
|
// components
|
||||||
import { SingleBoard } from "components/core/views/board-view/single-board";
|
import { SingleBoard } from "components/core/views/board-view/single-board";
|
||||||
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// icons
|
// icons
|
||||||
import { StateGroupIcon } from "components/icons";
|
import { StateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
@ -16,6 +23,8 @@ type Props = {
|
|||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
|
myIssueProjectId?: string | null;
|
||||||
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
states: IState[] | undefined;
|
states: IState[] | undefined;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
@ -30,18 +39,42 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
|
myIssueProjectId,
|
||||||
|
handleMyIssueOpen,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
states,
|
states,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
viewProps,
|
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 { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
console.log("viewProps", viewProps);
|
console.log("viewProps", viewProps);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<IssuePeekOverview
|
||||||
|
handleMutation={() =>
|
||||||
|
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
|
||||||
|
}
|
||||||
|
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
|
||||||
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
|
readOnly={disableUserActions}
|
||||||
|
/>
|
||||||
{groupedIssues ? (
|
{groupedIssues ? (
|
||||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
|
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
|
||||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||||
@ -65,6 +98,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
openIssuesListModal={openIssuesListModal ?? null}
|
openIssuesListModal={openIssuesListModal ?? null}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={removeIssue}
|
removeIssue={removeIssue}
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
|
@ -26,6 +26,7 @@ type Props = {
|
|||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
@ -42,6 +43,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
|
handleMyIssueOpen,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
@ -50,7 +52,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
// collapse/expand
|
// collapse/expand
|
||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
const { displayFilters, groupedIssues, properties } = viewProps;
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { cycleId, moduleId } = router.query;
|
const { cycleId, moduleId } = router.query;
|
||||||
@ -135,6 +137,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
if (removeIssue && issue.bridge_id)
|
if (removeIssue && issue.bridge_id)
|
||||||
removeIssue(issue.bridge_id, issue.id);
|
removeIssue(issue.bridge_id, issue.id);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
@ -58,6 +57,7 @@ type Props = {
|
|||||||
index: number;
|
index: number;
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
makeIssueCopy: () => void;
|
makeIssueCopy: () => void;
|
||||||
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
@ -75,6 +75,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
index,
|
index,
|
||||||
editIssue,
|
editIssue,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
|
handleMyIssueOpen,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
@ -187,6 +188,17 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
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;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -286,16 +298,22 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
|
||||||
<a className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{properties.key && (
|
{properties.key && (
|
||||||
<div className="text-xs font-medium text-custom-text-200">
|
<div className="text-xs font-medium text-custom-text-200">
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h5 className="text-sm break-words line-clamp-2">{issue.name}</h5>
|
<button
|
||||||
</a>
|
type="button"
|
||||||
</Link>
|
className="text-sm text-left break-words line-clamp-2"
|
||||||
|
onClick={openPeekOverview}
|
||||||
|
>
|
||||||
|
{issue.name}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 text-xs ${
|
className={`flex items-center gap-2 text-xs ${
|
||||||
isDropdownActive ? "" : "overflow-x-scroll"
|
isDropdownActive ? "" : "overflow-x-scroll"
|
||||||
|
@ -12,6 +12,7 @@ import issuesService from "services/issues.service";
|
|||||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||||
// components
|
// components
|
||||||
import { SingleCalendarDate, CalendarHeader } from "components/core";
|
import { SingleCalendarDate, CalendarHeader } from "components/core";
|
||||||
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "components/ui";
|
import { Spinner } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -60,7 +61,8 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
const { calendarIssues, params, setDisplayFilters } = useCalendarIssuesView();
|
const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } =
|
||||||
|
useCalendarIssuesView();
|
||||||
|
|
||||||
const totalDate = eachDayOfInterval({
|
const totalDate = eachDayOfInterval({
|
||||||
start: calendarDates.startDate,
|
start: calendarDates.startDate,
|
||||||
@ -160,84 +162,95 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisplayFilters({
|
if (!displayFilters || displayFilters.calendar_date_range === "")
|
||||||
calendar_date_range: `${renderDateFormat(startOfWeek(currentDate))};after,${renderDateFormat(
|
setDisplayFilters({
|
||||||
lastDayOfWeek(currentDate)
|
calendar_date_range: `${renderDateFormat(
|
||||||
)};before`,
|
startOfWeek(currentDate)
|
||||||
});
|
)};after,${renderDateFormat(lastDayOfWeek(currentDate))};before`,
|
||||||
}, [currentDate, setDisplayFilters]);
|
});
|
||||||
|
}, [currentDate, displayFilters, setDisplayFilters]);
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
return calendarIssues ? (
|
return (
|
||||||
<div className="h-full overflow-y-auto">
|
<>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<IssuePeekOverview
|
||||||
<div className="h-full rounded-lg p-8 text-custom-text-200">
|
handleMutation={() => mutateIssues()}
|
||||||
<CalendarHeader
|
projectId={projectId?.toString() ?? ""}
|
||||||
isMonthlyView={isMonthlyView}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
setIsMonthlyView={setIsMonthlyView}
|
readOnly={disableUserActions}
|
||||||
showWeekEnds={showWeekEnds}
|
/>
|
||||||
setShowWeekEnds={setShowWeekEnds}
|
{calendarIssues ? (
|
||||||
currentDate={currentDate}
|
<div className="h-full overflow-y-auto">
|
||||||
setCurrentDate={setCurrentDate}
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
changeDateRange={changeDateRange}
|
<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
|
<div
|
||||||
key={index}
|
className={`grid auto-rows-[minmax(36px,1fr)] rounded-lg ${
|
||||||
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 ${
|
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||||
!isMonthlyView
|
|
||||||
? showWeekEnds
|
|
||||||
? (index + 1) % 7 === 0
|
|
||||||
? ""
|
|
||||||
: "border-r"
|
|
||||||
: (index + 1) % 5 === 0
|
|
||||||
? ""
|
|
||||||
: "border-r"
|
|
||||||
: ""
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>
|
{weeks.map((date, index) => (
|
||||||
{isMonthlyView
|
<div
|
||||||
? formatDate(date, "eee").substring(0, 3)
|
key={index}
|
||||||
: formatDate(date, "eee")}
|
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 ${
|
||||||
</span>
|
!isMonthlyView
|
||||||
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
|
? 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>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`grid h-full ${isMonthlyView ? "auto-rows-min" : ""} ${
|
className={`grid h-full ${isMonthlyView ? "auto-rows-min" : ""} ${
|
||||||
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||||
} `}
|
} `}
|
||||||
>
|
>
|
||||||
{currentViewDaysData.map((date, index) => (
|
{currentViewDaysData.map((date, index) => (
|
||||||
<SingleCalendarDate
|
<SingleCalendarDate
|
||||||
key={`${date}-${index}`}
|
key={`${date}-${index}`}
|
||||||
index={index}
|
index={index}
|
||||||
date={date}
|
date={date}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
addIssueToDate={addIssueToDate}
|
addIssueToDate={addIssueToDate}
|
||||||
isMonthlyView={isMonthlyView}
|
isMonthlyView={isMonthlyView}
|
||||||
showWeekEnds={showWeekEnds}
|
showWeekEnds={showWeekEnds}
|
||||||
user={user}
|
user={user}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
) : (
|
||||||
</div>
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
) : (
|
<Spinner />
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
</div>
|
||||||
<Spinner />
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
@ -158,6 +157,15 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
? Object.values(properties).some((value) => value === true)
|
? Object.values(properties).some((value) => value === true)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
const openPeekOverview = () => {
|
||||||
|
const { query } = router;
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekIssue: issue.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
@ -193,23 +201,27 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<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">
|
<button
|
||||||
{properties.key && (
|
type="button"
|
||||||
<Tooltip
|
className="flex w-full cursor-pointer flex-col items-start justify-center gap-1.5"
|
||||||
tooltipHeading="Issue ID"
|
onClick={openPeekOverview}
|
||||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
>
|
||||||
>
|
{properties.key && (
|
||||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
<Tooltip
|
||||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
tooltipHeading="Issue ID"
|
||||||
</span>
|
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||||
</Tooltip>
|
>
|
||||||
)}
|
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
<span className="text-xs text-custom-text-100">{truncateText(issue.name, 25)}</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
)}
|
||||||
</Link>
|
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
|
<span className="text-xs text-custom-text-100">{truncateText(issue.name, 25)}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</button>
|
||||||
|
|
||||||
{displayProperties && (
|
{displayProperties && (
|
||||||
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
|
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
|
||||||
{properties.priority && (
|
{properties.priority && (
|
||||||
|
@ -6,20 +6,24 @@ import { IssueGanttChartView } from "components/issues";
|
|||||||
import { ModuleIssuesGanttChartView } from "components/modules";
|
import { ModuleIssuesGanttChartView } from "components/modules";
|
||||||
import { ViewIssuesGanttChartView } from "components/views";
|
import { ViewIssuesGanttChartView } from "components/views";
|
||||||
|
|
||||||
export const GanttChartView = () => {
|
type Props = {
|
||||||
|
disableUserActions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GanttChartView: React.FC<Props> = ({ disableUserActions }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { cycleId, moduleId, viewId } = router.query;
|
const { cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{cycleId ? (
|
{cycleId ? (
|
||||||
<CycleIssuesGanttChartView />
|
<CycleIssuesGanttChartView disableUserActions={disableUserActions} />
|
||||||
) : moduleId ? (
|
) : moduleId ? (
|
||||||
<ModuleIssuesGanttChartView />
|
<ModuleIssuesGanttChartView disableUserActions={disableUserActions} />
|
||||||
) : viewId ? (
|
) : viewId ? (
|
||||||
<ViewIssuesGanttChartView />
|
<ViewIssuesGanttChartView disableUserActions={disableUserActions} />
|
||||||
) : (
|
) : (
|
||||||
<IssueGanttChartView />
|
<IssueGanttChartView disableUserActions={disableUserActions} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -19,7 +19,7 @@ import useIssuesProperties from "hooks/use-issue-properties";
|
|||||||
import useProjectMembers from "hooks/use-project-members";
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
// components
|
// components
|
||||||
import { FiltersList, AllViews } from "components/core";
|
import { FiltersList, AllViews } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal, IssuePeekOverview } from "components/issues";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
@ -462,6 +462,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
data={issueToDelete}
|
data={issueToDelete}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{areFiltersApplied && (
|
{areFiltersApplied && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
|
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
|
// components
|
||||||
import { SingleList } from "components/core/views/list-view/single-list";
|
import { SingleList } from "components/core/views/list-view/single-list";
|
||||||
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
|
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
|
||||||
|
|
||||||
@ -9,6 +16,8 @@ type Props = {
|
|||||||
addIssueToGroup: (groupTitle: string) => void;
|
addIssueToGroup: (groupTitle: string) => void;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
|
myIssueProjectId?: string | null;
|
||||||
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
disableAddIssueOption?: boolean;
|
disableAddIssueOption?: boolean;
|
||||||
@ -23,16 +32,39 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
disableUserActions,
|
disableUserActions,
|
||||||
disableAddIssueOption = false,
|
disableAddIssueOption = false,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
|
handleMyIssueOpen,
|
||||||
|
myIssueProjectId,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
states,
|
states,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
viewProps,
|
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 { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<IssuePeekOverview
|
||||||
|
handleMutation={() =>
|
||||||
|
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
|
||||||
|
}
|
||||||
|
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
|
||||||
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
|
readOnly={disableUserActions}
|
||||||
|
/>
|
||||||
{groupedIssues && (
|
{groupedIssues && (
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
{Object.keys(groupedIssues).map((singleGroup) => {
|
{Object.keys(groupedIssues).map((singleGroup) => {
|
||||||
@ -51,6 +83,7 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
currentState={currentState}
|
currentState={currentState}
|
||||||
addIssueToGroup={() => addIssueToGroup(singleGroup)}
|
addIssueToGroup={() => addIssueToGroup(singleGroup)}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
openIssuesListModal={openIssuesListModal}
|
openIssuesListModal={openIssuesListModal}
|
||||||
removeIssue={removeIssue}
|
removeIssue={removeIssue}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
|
@ -61,6 +61,7 @@ type Props = {
|
|||||||
makeIssueCopy: () => void;
|
makeIssueCopy: () => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
@ -76,6 +77,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
removeIssue,
|
removeIssue,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
|
handleMyIssueOpen,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
@ -178,6 +180,16 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
||||||
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
|
: `/${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 =
|
const isNotAllowed =
|
||||||
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
|
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
|
||||||
|
|
||||||
@ -220,23 +232,27 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
|
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||||
<Link href={issuePath}>
|
<div className="group relative flex items-center gap-2">
|
||||||
<a className="group relative flex items-center gap-2">
|
{properties.key && (
|
||||||
{properties.key && (
|
<Tooltip
|
||||||
<Tooltip
|
tooltipHeading="Issue ID"
|
||||||
tooltipHeading="Issue ID"
|
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
>
|
||||||
>
|
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
</span>
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
|
||||||
<span className="truncate text-[0.825rem] text-custom-text-100">{issue.name}</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
)}
|
||||||
</Link>
|
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="truncate text-[0.825rem] text-custom-text-100"
|
||||||
|
onClick={() => openPeekOverview(issue)}
|
||||||
|
>
|
||||||
|
{issue.name}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -41,6 +41,7 @@ type Props = {
|
|||||||
addIssueToGroup: () => void;
|
addIssueToGroup: () => void;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
disableAddIssueOption?: boolean;
|
disableAddIssueOption?: boolean;
|
||||||
@ -55,6 +56,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
addIssueToGroup,
|
addIssueToGroup,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
|
handleMyIssueOpen,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
disableAddIssueOption = false,
|
disableAddIssueOption = false,
|
||||||
@ -251,6 +253,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
editIssue={() => handleIssueAction(issue, "edit")}
|
editIssue={() => handleIssueAction(issue, "edit")}
|
||||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
if (removeIssue !== null && issue.bridge_id)
|
if (removeIssue !== null && issue.bridge_id)
|
||||||
removeIssue(issue.bridge_id, issue.id);
|
removeIssue(issue.bridge_id, issue.id);
|
||||||
|
@ -8,11 +8,15 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
|
|||||||
import useProjectDetails from "hooks/use-project-details";
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
// components
|
// components
|
||||||
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||||
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
|
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export const CycleIssuesGanttChartView = () => {
|
type Props = {
|
||||||
|
disableUserActions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CycleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
|
|
||||||
@ -30,23 +34,31 @@ export const CycleIssuesGanttChartView = () => {
|
|||||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<>
|
||||||
<GanttChartRoot
|
<IssuePeekOverview
|
||||||
border={false}
|
handleMutation={() => mutateGanttIssues()}
|
||||||
title="Issues"
|
projectId={projectId?.toString() ?? ""}
|
||||||
loaderTitle="Issues"
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
|
readOnly={disableUserActions}
|
||||||
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 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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Tab, Transition, Popover } from "@headlessui/react";
|
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||||
// react colors
|
// react colors
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
|
// hooks
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// types
|
// types
|
||||||
import { Props } from "./types";
|
import { Props } from "./types";
|
||||||
// emojis
|
// emojis
|
||||||
@ -36,6 +38,8 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
|
|
||||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const emojiPickerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecentEmojis(getRecentEmojis());
|
setRecentEmojis(getRecentEmojis());
|
||||||
}, []);
|
}, []);
|
||||||
@ -44,6 +48,8 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
if (!value || value?.length === 0) onChange(getRandomEmoji());
|
if (!value || value?.length === 0) onChange(getRandomEmoji());
|
||||||
}, [value, onChange]);
|
}, [value, onChange]);
|
||||||
|
|
||||||
|
useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative z-[1]">
|
<Popover className="relative z-[1]">
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
@ -63,7 +69,10 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
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">
|
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
||||||
<div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-custom-border-200 bg-custom-background-80 p-2 shadow-xl">
|
<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"
|
||||||
|
>
|
||||||
<Tab.Group as="div" className="flex h-full w-full flex-col">
|
<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">
|
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
|
||||||
{tabOptions.map((tab) => (
|
{tabOptions.map((tab) => (
|
||||||
|
@ -66,7 +66,7 @@ export const SingleEstimate: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gap-2 py-3">
|
<div className="gap-2 p-4 border-b border-custom-border-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">
|
<h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">
|
||||||
|
@ -83,3 +83,5 @@ export * from "./archive-icon";
|
|||||||
export * from "./clock-icon";
|
export * from "./clock-icon";
|
||||||
export * from "./bell-icon";
|
export * from "./bell-icon";
|
||||||
export * from "./single-comment-icon";
|
export * from "./single-comment-icon";
|
||||||
|
export * from "./related-icon";
|
||||||
|
export * from "./module-icon";
|
59
web/components/icons/module-icon.tsx
Normal file
59
web/components/icons/module-icon.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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
web/components/icons/related-icon.tsx
Normal file
41
web/components/icons/related-icon.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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,6 +66,8 @@ export const SelectRepository: React.FC<Props> = ({
|
|||||||
content: <p>{truncateText(repo.full_name, characterLimit)}</p>,
|
content: <p>{truncateText(repo.full_name, characterLimit)}</p>,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
|
if (userRepositories.length < 1) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -83,9 +83,7 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
|
|||||||
{projectIntegration ? (
|
{projectIntegration ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
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 ${
|
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`}
|
||||||
slackChannelAvailabilityToggle ? "bg-green-500" : "bg-gray-200"
|
|
||||||
}`}
|
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-checked
|
aria-checked
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -94,8 +92,8 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
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-5" : "translate-x-0"
|
slackChannelAvailabilityToggle ? "translate-x-3" : "translate-x-0"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -23,14 +23,7 @@ import {
|
|||||||
import { CreateStateModal } from "components/states";
|
import { CreateStateModal } from "components/states";
|
||||||
import { CreateLabelModal } from "components/labels";
|
import { CreateLabelModal } from "components/labels";
|
||||||
// ui
|
// ui
|
||||||
import {
|
import { CustomMenu, Input, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||||
CustomMenu,
|
|
||||||
Input,
|
|
||||||
Loader,
|
|
||||||
PrimaryButton,
|
|
||||||
SecondaryButton,
|
|
||||||
ToggleSwitch,
|
|
||||||
} from "components/ui";
|
|
||||||
import { TipTapEditor } from "components/tiptap";
|
import { TipTapEditor } from "components/tiptap";
|
||||||
// icons
|
// icons
|
||||||
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
@ -52,7 +45,7 @@ const defaultValues: Partial<IIssue> = {
|
|||||||
estimate_point: null,
|
estimate_point: null,
|
||||||
state: "",
|
state: "",
|
||||||
parent: null,
|
parent: null,
|
||||||
priority: null,
|
priority: "none",
|
||||||
assignees: [],
|
assignees: [],
|
||||||
assignees_list: [],
|
assignees_list: [],
|
||||||
labels: [],
|
labels: [],
|
||||||
|
@ -11,13 +11,21 @@ import { IIssue } from "types";
|
|||||||
|
|
||||||
export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
|
export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
const openPeekOverview = () => {
|
||||||
|
const { query } = router;
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekIssue: data.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center relative h-full w-full rounded cursor-pointer"
|
className="flex items-center relative h-full w-full rounded cursor-pointer"
|
||||||
style={{ backgroundColor: data?.state_detail?.color }}
|
style={{ backgroundColor: data?.state_detail?.color }}
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
|
onClick={openPeekOverview}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 left-0 h-full w-full bg-custom-background-100/50" />
|
<div className="absolute top-0 left-0 h-full w-full bg-custom-background-100/50" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -43,14 +51,22 @@ export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
|
|||||||
// rendering issues on gantt sidebar
|
// rendering issues on gantt sidebar
|
||||||
export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
|
export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const duration = findTotalDaysInRange(data?.start_date ?? "", data?.target_date ?? "", true);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative w-full flex items-center gap-2 h-full cursor-pointer"
|
className="relative w-full flex items-center gap-2 h-full cursor-pointer"
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
|
onClick={openPeekOverview}
|
||||||
>
|
>
|
||||||
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
|
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
|
||||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||||
|
@ -8,11 +8,15 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
|
|||||||
import useProjectDetails from "hooks/use-project-details";
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
// components
|
// components
|
||||||
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||||
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
|
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export const IssueGanttChartView = () => {
|
type Props = {
|
||||||
|
disableUserActions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueGanttChartView: React.FC<Props> = ({ disableUserActions }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -29,23 +33,31 @@ export const IssueGanttChartView = () => {
|
|||||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<>
|
||||||
<GanttChartRoot
|
<IssuePeekOverview
|
||||||
border={false}
|
handleMutation={() => mutateGanttIssues()}
|
||||||
title="Issues"
|
projectId={projectId?.toString() ?? ""}
|
||||||
loaderTitle="Issues"
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
|
readOnly={disableUserActions}
|
||||||
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 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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -57,7 +57,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
|||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
|
|
||||||
const { groupedIssues, mutateMyIssues, isEmpty, params } = useMyIssues(workspaceSlug?.toString());
|
const { groupedIssues, mutateMyIssues, isEmpty, params } = useMyIssues(workspaceSlug?.toString());
|
||||||
const { filters, setFilters, displayFilters, setDisplayFilters, properties } = useMyIssuesFilters(
|
const { filters, setFilters, displayFilters, properties } = useMyIssuesFilters(
|
||||||
workspaceSlug?.toString()
|
workspaceSlug?.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
|
|||||||
<span>
|
<span>
|
||||||
<PriorityIcon priority={priority} />
|
<PriorityIcon priority={priority} />
|
||||||
</span>
|
</span>
|
||||||
<span className="capitalize">{priority ?? "None"}</span>
|
<span className="capitalize">{priority}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { UseFormWatch } from "react-hook-form";
|
import { UseFormWatch } from "react-hook-form";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
// components
|
// components
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
// icons
|
// icons
|
||||||
@ -29,10 +31,11 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsBlockedModalOpen(false);
|
setIsBlockedModalOpen(false);
|
||||||
@ -62,21 +65,39 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const newBlocked = [...watch("blocked_issues"), ...selectedIssues];
|
if (!user) return;
|
||||||
|
|
||||||
|
issuesService
|
||||||
|
.createIssueRelation(workspaceSlug as string, projectId as string, issueId as string, user, {
|
||||||
|
related_list: [
|
||||||
|
...selectedIssues.map((issue) => ({
|
||||||
|
issue: issueId as string,
|
||||||
|
relation_type: "blocked_by" as const,
|
||||||
|
related_issue_detail: issue.blocked_issue_detail,
|
||||||
|
related_issue: issue.blocked_issue_detail.id,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
submitChanges({
|
||||||
|
related_issues: [
|
||||||
|
...watch("related_issues")?.filter((i) => i.relation_type !== "blocked_by"),
|
||||||
|
...response,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
submitChanges({
|
|
||||||
blocked_issues: newBlocked,
|
|
||||||
blocks_list: newBlocked.map((i) => i.blocked_issue_detail?.id ?? ""),
|
|
||||||
});
|
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const blockedByIssue = watch("related_issues")?.filter((i) => i.relation_type === "blocked_by");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
isOpen={isBlockedModalOpen}
|
isOpen={isBlockedModalOpen}
|
||||||
handleClose={() => setIsBlockedModalOpen(false)}
|
handleClose={() => setIsBlockedModalOpen(false)}
|
||||||
searchParams={{ blocker_blocked_by: true, issue_id: issueId }}
|
searchParams={{ issue_relation: true, issue_id: issueId }}
|
||||||
handleOnSubmit={onSubmit}
|
handleOnSubmit={onSubmit}
|
||||||
workspaceLevelToggle
|
workspaceLevelToggle
|
||||||
/>
|
/>
|
||||||
@ -87,33 +108,42 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 sm:basis-1/2">
|
<div className="space-y-1 sm:basis-1/2">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{watch("blocked_issues") && watch("blocked_issues").length > 0
|
{blockedByIssue && blockedByIssue.length > 0
|
||||||
? watch("blocked_issues").map((issue) => (
|
? blockedByIssue.map((relation) => (
|
||||||
<div
|
<div
|
||||||
key={issue.blocked_issue_detail?.id}
|
key={relation.related_issue_detail?.id}
|
||||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${issue.blocked_issue_detail?.project_detail.id}/issues/${issue.blocked_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${relation.related_issue_detail?.project_detail.id}/issues/${relation.related_issue_detail?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<BlockedIcon height={10} width={10} />
|
<BlockedIcon height={10} width={10} />
|
||||||
{`${issue.blocked_issue_detail?.project_detail.identifier}-${issue.blocked_issue_detail?.sequence_id}`}
|
{`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedBlocked = watch("blocked_issues").filter(
|
const updatedRelations = watch("related_issues")?.filter(
|
||||||
(i) => i.blocked_issue_detail?.id !== issue.blocked_issue_detail?.id
|
(i) => i.id !== relation.id
|
||||||
);
|
);
|
||||||
|
|
||||||
submitChanges({
|
submitChanges({
|
||||||
blocked_issues: updatedBlocked,
|
related_issues: updatedRelations,
|
||||||
blocks_list: updatedBlocked.map((i) => i.blocked_issue_detail?.id ?? ""),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
issuesService.deleteIssueRelation(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
issueId as string,
|
||||||
|
relation.id,
|
||||||
|
user
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XMarkIcon className="h-2 w-2" />
|
<XMarkIcon className="h-2 w-2" />
|
||||||
|
@ -6,8 +6,11 @@ import { useRouter } from "next/router";
|
|||||||
import { UseFormWatch } from "react-hook-form";
|
import { UseFormWatch } from "react-hook-form";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
// components
|
// components
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
// icons
|
// icons
|
||||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
import { BlockerIcon } from "components/icons";
|
import { BlockerIcon } from "components/icons";
|
||||||
@ -29,15 +32,19 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsBlockerModalOpen(false);
|
setIsBlockerModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const blockerIssue =
|
||||||
|
watch("issue_relations")?.filter((i) => i.relation_type === "blocked_by") || [];
|
||||||
|
|
||||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -62,12 +69,33 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const newBlockers = [...watch("blocker_issues"), ...selectedIssues];
|
if (!user) return;
|
||||||
|
|
||||||
|
issuesService
|
||||||
|
.createIssueRelation(workspaceSlug as string, projectId as string, issueId as string, user, {
|
||||||
|
related_list: [
|
||||||
|
...selectedIssues.map((issue) => ({
|
||||||
|
issue: issue.blocker_issue_detail.id,
|
||||||
|
relation_type: "blocked_by" as const,
|
||||||
|
related_issue: issueId as string,
|
||||||
|
related_issue_detail: issue.blocker_issue_detail,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
submitChanges({
|
||||||
|
issue_relations: [
|
||||||
|
...blockerIssue,
|
||||||
|
...(response ?? []).map((i: any) => ({
|
||||||
|
id: i.id,
|
||||||
|
relation_type: i.relation_type,
|
||||||
|
issue_detail: i.related_issue_detail,
|
||||||
|
issue: i.related_issue,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
submitChanges({
|
|
||||||
blocker_issues: newBlockers,
|
|
||||||
blockers_list: newBlockers.map((i) => i.blocker_issue_detail?.id ?? ""),
|
|
||||||
});
|
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,7 +104,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
isOpen={isBlockerModalOpen}
|
isOpen={isBlockerModalOpen}
|
||||||
handleClose={() => setIsBlockerModalOpen(false)}
|
handleClose={() => setIsBlockerModalOpen(false)}
|
||||||
searchParams={{ blocker_blocked_by: true, issue_id: issueId }}
|
searchParams={{ issue_relation: true, issue_id: issueId }}
|
||||||
handleOnSubmit={onSubmit}
|
handleOnSubmit={onSubmit}
|
||||||
workspaceLevelToggle
|
workspaceLevelToggle
|
||||||
/>
|
/>
|
||||||
@ -87,35 +115,42 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 sm:basis-1/2">
|
<div className="space-y-1 sm:basis-1/2">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{watch("blocker_issues") && watch("blocker_issues").length > 0
|
{blockerIssue && blockerIssue.length > 0
|
||||||
? watch("blocker_issues").map((issue) => (
|
? blockerIssue.map((relation) => (
|
||||||
<div
|
<div
|
||||||
key={issue.blocker_issue_detail?.id}
|
key={relation.issue_detail?.id}
|
||||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${issue.blocker_issue_detail?.project_detail.id}/issues/${issue.blocker_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${relation.issue_detail?.project_detail.id}/issues/${relation.issue_detail?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<BlockerIcon height={10} width={10} />
|
<BlockerIcon height={10} width={10} />
|
||||||
{`${issue.blocker_issue_detail?.project_detail.identifier}-${issue.blocker_issue_detail?.sequence_id}`}
|
{`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedBlockers = watch("blocker_issues").filter(
|
const updatedBlockers = blockerIssue.filter(
|
||||||
(i) => i.blocker_issue_detail?.id !== issue.blocker_issue_detail?.id
|
(i) => i.issue_detail?.id !== relation.issue_detail?.id
|
||||||
);
|
);
|
||||||
|
|
||||||
submitChanges({
|
submitChanges({
|
||||||
blocker_issues: updatedBlockers,
|
issue_relations: updatedBlockers,
|
||||||
blockers_list: updatedBlockers.map(
|
|
||||||
(i) => i.blocker_issue_detail?.id ?? ""
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
issuesService.deleteIssueRelation(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
relation.issue_detail?.id as string,
|
||||||
|
relation.id,
|
||||||
|
user
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XMarkIcon className="h-2 w-2" />
|
<XMarkIcon className="h-2 w-2" />
|
||||||
|
172
web/components/issues/sidebar-select/duplicate.tsx
Normal file
172
web/components/issues/sidebar-select/duplicate.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// react-hook-form
|
||||||
|
import { UseFormWatch } from "react-hook-form";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
// icons
|
||||||
|
import { X, CopyPlus } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { BlockerIcon } from "components/icons";
|
||||||
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// types
|
||||||
|
import { BlockeIssueDetail, IIssue, ISearchIssueResponse } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issueId?: string;
|
||||||
|
submitChanges: (formData: Partial<IIssue>) => void;
|
||||||
|
watch: UseFormWatch<IIssue>;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarDuplicateSelect: React.FC<Props> = (props) => {
|
||||||
|
const { issueId, submitChanges, watch, disabled = false } = props;
|
||||||
|
|
||||||
|
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsDuplicateModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||||
|
if (data.length === 0) {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Please select at least one issue.",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIssues: { blocker_issue_detail: BlockeIssueDetail }[] = data.map((i) => ({
|
||||||
|
blocker_issue_detail: {
|
||||||
|
id: i.id,
|
||||||
|
name: i.name,
|
||||||
|
sequence_id: i.sequence_id,
|
||||||
|
project_detail: {
|
||||||
|
id: i.project_id,
|
||||||
|
identifier: i.project__identifier,
|
||||||
|
name: i.project__name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
issuesService
|
||||||
|
.createIssueRelation(workspaceSlug as string, projectId as string, issueId as string, user, {
|
||||||
|
related_list: [
|
||||||
|
...selectedIssues.map((issue) => ({
|
||||||
|
issue: issueId as string,
|
||||||
|
related_issue_detail: issue.blocker_issue_detail,
|
||||||
|
related_issue: issue.blocker_issue_detail.id,
|
||||||
|
relation_type: "duplicate" as const,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
submitChanges({
|
||||||
|
related_issues: [...watch("related_issues"), ...(response ?? [])],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const duplicateIssuesRelation = [
|
||||||
|
...(watch("related_issues")?.filter((i) => i.relation_type === "duplicate") ?? []),
|
||||||
|
...(watch("issue_relations") ?? [])
|
||||||
|
?.filter((i) => i.relation_type === "duplicate")
|
||||||
|
.map((i) => ({
|
||||||
|
...i,
|
||||||
|
related_issue_detail: i.issue_detail,
|
||||||
|
related_issue: i.issue_detail?.id,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExistingIssuesListModal
|
||||||
|
isOpen={isDuplicateModalOpen}
|
||||||
|
handleClose={() => setIsDuplicateModalOpen(false)}
|
||||||
|
searchParams={{ issue_relation: true, issue_id: issueId }}
|
||||||
|
handleOnSubmit={onSubmit}
|
||||||
|
workspaceLevelToggle
|
||||||
|
/>
|
||||||
|
<div className="flex flex-wrap items-start py-2">
|
||||||
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||||
|
<CopyPlus className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<p>Duplicate</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1 sm:basis-1/2">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{duplicateIssuesRelation && duplicateIssuesRelation.length > 0
|
||||||
|
? duplicateIssuesRelation.map((relation) => (
|
||||||
|
<div
|
||||||
|
key={relation.related_issue_detail?.id}
|
||||||
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${relation.related_issue_detail?.project_detail.id}/issues/${relation.related_issue_detail?.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<BlockerIcon height={10} width={10} />
|
||||||
|
{`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
|
onClick={() => {
|
||||||
|
const updatedBlockers = duplicateIssuesRelation.filter(
|
||||||
|
(i) => i.related_issue_detail?.id !== relation.related_issue_detail?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
submitChanges({
|
||||||
|
related_issues: updatedBlockers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
issuesService.deleteIssueRelation(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
issueId as string,
|
||||||
|
relation.id,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-2 w-2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`bg-custom-background-80 text-xs text-custom-text-200 rounded px-2.5 py-0.5 ${
|
||||||
|
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||||
|
}`}
|
||||||
|
onClick={() => setIsDuplicateModalOpen(true)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
Select issues
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -8,3 +8,5 @@ export * from "./module";
|
|||||||
export * from "./parent";
|
export * from "./parent";
|
||||||
export * from "./priority";
|
export * from "./priority";
|
||||||
export * from "./state";
|
export * from "./state";
|
||||||
|
export * from "./duplicate";
|
||||||
|
export * from "./relates-to";
|
||||||
|
172
web/components/issues/sidebar-select/relates-to.tsx
Normal file
172
web/components/issues/sidebar-select/relates-to.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// react-hook-form
|
||||||
|
import { UseFormWatch } from "react-hook-form";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
// icons
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { BlockerIcon, RelatedIcon } from "components/icons";
|
||||||
|
// components
|
||||||
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// types
|
||||||
|
import { BlockeIssueDetail, IIssue, ISearchIssueResponse } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issueId?: string;
|
||||||
|
submitChanges: (formData: Partial<IIssue>) => void;
|
||||||
|
watch: UseFormWatch<IIssue>;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarRelatesSelect: React.FC<Props> = (props) => {
|
||||||
|
const { issueId, submitChanges, watch, disabled = false } = props;
|
||||||
|
|
||||||
|
const [isRelatesToModalOpen, setIsRelatesToModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsRelatesToModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||||
|
if (data.length === 0) {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Please select at least one issue.",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIssues: { blocker_issue_detail: BlockeIssueDetail }[] = data.map((i) => ({
|
||||||
|
blocker_issue_detail: {
|
||||||
|
id: i.id,
|
||||||
|
name: i.name,
|
||||||
|
sequence_id: i.sequence_id,
|
||||||
|
project_detail: {
|
||||||
|
id: i.project_id,
|
||||||
|
identifier: i.project__identifier,
|
||||||
|
name: i.project__name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
issuesService
|
||||||
|
.createIssueRelation(workspaceSlug as string, projectId as string, issueId as string, user, {
|
||||||
|
related_list: [
|
||||||
|
...selectedIssues.map((issue) => ({
|
||||||
|
issue: issueId as string,
|
||||||
|
related_issue_detail: issue.blocker_issue_detail,
|
||||||
|
related_issue: issue.blocker_issue_detail.id,
|
||||||
|
relation_type: "relates_to" as const,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
submitChanges({
|
||||||
|
related_issues: [...watch("related_issues"), ...(response ?? [])],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedToIssueRelation = [
|
||||||
|
...(watch("related_issues")?.filter((i) => i.relation_type === "relates_to") ?? []),
|
||||||
|
...(watch("issue_relations") ?? [])
|
||||||
|
?.filter((i) => i.relation_type === "relates_to")
|
||||||
|
.map((i) => ({
|
||||||
|
...i,
|
||||||
|
related_issue_detail: i.issue_detail,
|
||||||
|
related_issue: i.issue_detail?.id,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExistingIssuesListModal
|
||||||
|
isOpen={isRelatesToModalOpen}
|
||||||
|
handleClose={() => setIsRelatesToModalOpen(false)}
|
||||||
|
searchParams={{ issue_relation: true, issue_id: issueId }}
|
||||||
|
handleOnSubmit={onSubmit}
|
||||||
|
workspaceLevelToggle
|
||||||
|
/>
|
||||||
|
<div className="flex flex-wrap items-start py-2">
|
||||||
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||||
|
<RelatedIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<p>Relates to</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1 sm:basis-1/2">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{relatedToIssueRelation && relatedToIssueRelation.length > 0
|
||||||
|
? relatedToIssueRelation.map((relation) => (
|
||||||
|
<div
|
||||||
|
key={relation.related_issue_detail?.id}
|
||||||
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${relation.related_issue_detail?.project_detail.id}/issues/${relation.related_issue_detail?.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<BlockerIcon height={10} width={10} />
|
||||||
|
{`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
|
onClick={() => {
|
||||||
|
const updatedBlockers = relatedToIssueRelation.filter(
|
||||||
|
(i) => i.related_issue_detail?.id !== relation.related_issue_detail?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
submitChanges({
|
||||||
|
related_issues: updatedBlockers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
issuesService.deleteIssueRelation(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
issueId as string,
|
||||||
|
relation.id,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-2 w-2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`bg-custom-background-80 text-xs text-custom-text-200 rounded px-2.5 py-0.5 ${
|
||||||
|
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||||
|
}`}
|
||||||
|
onClick={() => setIsRelatesToModalOpen(true)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
Select issues
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -30,6 +30,8 @@ import {
|
|||||||
SidebarStateSelect,
|
SidebarStateSelect,
|
||||||
SidebarEstimateSelect,
|
SidebarEstimateSelect,
|
||||||
SidebarLabelSelect,
|
SidebarLabelSelect,
|
||||||
|
SidebarDuplicateSelect,
|
||||||
|
SidebarRelatesSelect,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker, Icon } from "components/ui";
|
import { CustomDatePicker, Icon } from "components/ui";
|
||||||
@ -76,6 +78,8 @@ type Props = {
|
|||||||
| "delete"
|
| "delete"
|
||||||
| "all"
|
| "all"
|
||||||
| "subscribe"
|
| "subscribe"
|
||||||
|
| "duplicate"
|
||||||
|
| "relates_to"
|
||||||
)[];
|
)[];
|
||||||
uneditable?: boolean;
|
uneditable?: boolean;
|
||||||
};
|
};
|
||||||
@ -464,7 +468,19 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
||||||
<SidebarBlockerSelect
|
<SidebarBlockerSelect
|
||||||
issueId={issueId as string}
|
issueId={issueId as string}
|
||||||
submitChanges={submitChanges}
|
submitChanges={(data: any) => {
|
||||||
|
mutate<IIssue>(
|
||||||
|
ISSUE_DETAILS(issueId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
/>
|
/>
|
||||||
@ -472,7 +488,59 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
||||||
<SidebarBlockedSelect
|
<SidebarBlockedSelect
|
||||||
issueId={issueId as string}
|
issueId={issueId as string}
|
||||||
submitChanges={submitChanges}
|
submitChanges={(data: any) => {
|
||||||
|
mutate<IIssue>(
|
||||||
|
ISSUE_DETAILS(issueId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
watch={watchIssue}
|
||||||
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("duplicate")) && (
|
||||||
|
<SidebarDuplicateSelect
|
||||||
|
issueId={issueId as string}
|
||||||
|
submitChanges={(data: any) => {
|
||||||
|
mutate<IIssue>(
|
||||||
|
ISSUE_DETAILS(issueId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
watch={watchIssue}
|
||||||
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("relates_to")) && (
|
||||||
|
<SidebarRelatesSelect
|
||||||
|
issueId={issueId as string}
|
||||||
|
submitChanges={(data: any) => {
|
||||||
|
mutate<IIssue>(
|
||||||
|
ISSUE_DETAILS(issueId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
/>
|
/>
|
||||||
|
@ -17,7 +17,7 @@ import issuesService from "services/issues.service";
|
|||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { Component } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabels } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -132,7 +132,7 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex scroll-m-8 items-center gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 ${
|
className={`flex scroll-m-8 items-center gap-2 rounded border border-custom-border-200 bg-custom-background-100 px-3.5 py-2 ${
|
||||||
labelForm ? "" : "hidden"
|
labelForm ? "" : "hidden"
|
||||||
}`}
|
}`}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -146,18 +146,12 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
|||||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<Component
|
||||||
className="h-5 w-5 rounded"
|
className="h-4 w-4 text-custom-text-100 flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: watch("color"),
|
color: watch("color"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ChevronDownIcon
|
|
||||||
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
|
|
||||||
open ? "text-gray-600" : "text-gray-400"
|
|
||||||
}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
|
@ -13,12 +13,12 @@ import { CustomMenu } from "components/ui";
|
|||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
RectangleGroupIcon,
|
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { Component, X } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssueLabels } from "types";
|
import { ICurrentUserResponse, IIssueLabels } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -76,20 +76,18 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<Disclosure
|
<Disclosure
|
||||||
as="div"
|
as="div"
|
||||||
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 text-custom-text-100"
|
className="rounded border border-custom-border-200 bg-custom-background-100 px-3.5 py-3 text-custom-text-100"
|
||||||
defaultOpen
|
defaultOpen
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex cursor-pointer items-center justify-between gap-2">
|
<div className="flex cursor-pointer items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>
|
<Component className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
|
||||||
<RectangleGroupIcon className="h-4 w-4" />
|
|
||||||
</span>
|
|
||||||
<h6>{label.name}</h6>
|
<h6>{label.name}</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CustomMenu ellipsis>
|
<CustomMenu ellipsis buttonClassName="!text-custom-sidebar-text-400">
|
||||||
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@ -112,7 +110,9 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
|||||||
<Disclosure.Button>
|
<Disclosure.Button>
|
||||||
<span>
|
<span>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={`h-4 w-4 text-custom-text-100 ${!open ? "rotate-90 transform" : ""}`}
|
className={`h-4 w-4 text-custom-sidebar-text-400 ${
|
||||||
|
!open ? "rotate-90 transform" : ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
@ -128,15 +128,15 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
|||||||
leaveTo="transform opacity-0"
|
leaveTo="transform opacity-0"
|
||||||
>
|
>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<div className="mt-3 ml-6 space-y-3">
|
<div className="mt-2.5 ml-6">
|
||||||
{labelChildren.map((child) => (
|
{labelChildren.map((child) => (
|
||||||
<div
|
<div
|
||||||
key={child.id}
|
key={child.id}
|
||||||
className="group flex items-center justify-between rounded-md border border-custom-border-200 p-2 text-sm"
|
className="group flex items-center justify-between border-b border-custom-border-200 px-4 py-2.5 text-sm last:border-0"
|
||||||
>
|
>
|
||||||
<h5 className="flex items-center gap-3">
|
<h5 className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
child.color && child.color !== "" ? child.color : "#000000",
|
child.color && child.color !== "" ? child.color : "#000000",
|
||||||
@ -144,27 +144,38 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
{child.name}
|
{child.name}
|
||||||
</h5>
|
</h5>
|
||||||
<div className="pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
<div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||||
<CustomMenu ellipsis>
|
<div className="h-4 w-4">
|
||||||
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
|
<CustomMenu
|
||||||
<span className="flex items-center justify-start gap-2">
|
customButton={
|
||||||
<XMarkIcon className="h-4 w-4" />
|
<div className="h-4 w-4">
|
||||||
<span>Remove from group</span>
|
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
</span>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
}
|
||||||
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
|
>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
|
||||||
<PencilIcon className="h-4 w-4" />
|
<span className="flex items-center justify-start gap-2">
|
||||||
<span>Edit label</span>
|
<XMarkIcon className="h-4 w-4" />
|
||||||
</span>
|
<span>Remove from group</span>
|
||||||
</CustomMenu.MenuItem>
|
</span>
|
||||||
<CustomMenu.MenuItem onClick={handleLabelDelete}>
|
</CustomMenu.MenuItem>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
|
||||||
<TrashIcon className="h-4 w-4" />
|
<span className="flex items-center justify-start gap-2">
|
||||||
<span>Delete label</span>
|
<PencilIcon className="h-4 w-4" />
|
||||||
</span>
|
<span>Edit label</span>
|
||||||
</CustomMenu.MenuItem>
|
</span>
|
||||||
</CustomMenu>
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-start gap-2"
|
||||||
|
onClick={handleLabelDelete}
|
||||||
|
>
|
||||||
|
<X className="h-[18px] w-[18px] text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -5,7 +5,8 @@ import { CustomMenu } from "components/ui";
|
|||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabels } from "types";
|
||||||
//icons
|
//icons
|
||||||
import { RectangleGroupIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { RectangleGroupIcon, PencilIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { Component, X } from "lucide-react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: IIssueLabels;
|
label: IIssueLabels;
|
||||||
@ -20,8 +21,8 @@ export const SingleLabel: React.FC<Props> = ({
|
|||||||
editLabel,
|
editLabel,
|
||||||
handleLabelDelete,
|
handleLabelDelete,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="gap-2 space-y-3 divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5">
|
<div className="gap-2 space-y-3 divide-y divide-custom-border-200 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-2.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="group flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||||
@ -31,26 +32,36 @@ export const SingleLabel: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
<h6 className="text-sm">{label.name}</h6>
|
<h6 className="text-sm">{label.name}</h6>
|
||||||
</div>
|
</div>
|
||||||
<CustomMenu ellipsis>
|
<div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||||
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
<div className="h-4 w-4">
|
||||||
<span className="flex items-center justify-start gap-2">
|
<CustomMenu
|
||||||
<RectangleGroupIcon className="h-4 w-4" />
|
customButton={
|
||||||
<span>Convert to group</span>
|
<div className="h-4 w-4">
|
||||||
</span>
|
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
</CustomMenu.MenuItem>
|
</div>
|
||||||
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
|
}
|
||||||
<span className="flex items-center justify-start gap-2">
|
>
|
||||||
<PencilIcon className="h-4 w-4" />
|
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
||||||
<span>Edit label</span>
|
<span className="flex items-center justify-start gap-2">
|
||||||
</span>
|
<RectangleGroupIcon className="h-4 w-4" />
|
||||||
</CustomMenu.MenuItem>
|
<span>Convert to group</span>
|
||||||
<CustomMenu.MenuItem onClick={handleLabelDelete}>
|
</span>
|
||||||
<span className="flex items-center justify-start gap-2">
|
</CustomMenu.MenuItem>
|
||||||
<TrashIcon className="h-4 w-4" />
|
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
|
||||||
<span>Delete label</span>
|
<span className="flex items-center justify-start gap-2">
|
||||||
</span>
|
<PencilIcon className="h-4 w-4" />
|
||||||
</CustomMenu.MenuItem>
|
<span>Edit label</span>
|
||||||
</CustomMenu>
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button className="flex items-center justify-start gap-2" onClick={handleLabelDelete}>
|
||||||
|
<X className="h-[18px] w-[18px] text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
@ -10,13 +8,13 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
|
|||||||
import useProjectDetails from "hooks/use-project-details";
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
// components
|
// components
|
||||||
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||||
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
|
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {};
|
type Props = { disableUserActions: boolean };
|
||||||
|
|
||||||
export const ModuleIssuesGanttChartView: FC<Props> = ({}) => {
|
export const ModuleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||||
|
|
||||||
@ -34,23 +32,31 @@ export const ModuleIssuesGanttChartView: FC<Props> = ({}) => {
|
|||||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<>
|
||||||
<GanttChartRoot
|
<IssuePeekOverview
|
||||||
border={false}
|
handleMutation={() => mutateGanttIssues()}
|
||||||
title="Issues"
|
projectId={projectId?.toString() ?? ""}
|
||||||
loaderTitle="Issues"
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
|
readOnly={disableUserActions}
|
||||||
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 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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
export * from "./create-project-modal";
|
export * from "./create-project-modal";
|
||||||
export * from "./delete-project-modal";
|
export * from "./delete-project-modal";
|
||||||
export * from "./sidebar-list";
|
export * from "./sidebar-list";
|
||||||
export * from "./settings-header";
|
export * from "./settings-sidebar";
|
||||||
export * from "./single-integration-card";
|
export * from "./single-integration-card";
|
||||||
export * from "./single-project-card";
|
export * from "./single-project-card";
|
||||||
export * from "./single-sidebar-project";
|
export * from "./single-sidebar-project";
|
||||||
export * from "./confirm-project-leave-modal";
|
export * from "./confirm-project-leave-modal";
|
||||||
|
export * from "./member-select";
|
||||||
|
74
web/components/project/member-select.tsx
Normal file
74
web/components/project/member-select.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import projectService from "services/project.service";
|
||||||
|
// ui
|
||||||
|
import { Avatar, CustomSearchSelect } from "components/ui";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: any;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MemberSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { data: members } = useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = members?.map((member) => ({
|
||||||
|
value: member.member.id,
|
||||||
|
query: member.member.display_name,
|
||||||
|
content: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.display_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectedOption = members?.find((m) => m.member.id === value)?.member;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomSearchSelect
|
||||||
|
value={value}
|
||||||
|
label={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedOption && <Avatar user={selectedOption} />}
|
||||||
|
{selectedOption ? (
|
||||||
|
selectedOption?.display_name
|
||||||
|
) : (
|
||||||
|
<span className="text-sm py-0.5 text-custom-text-200">Select</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonClassName="!px-3 !py-2"
|
||||||
|
options={
|
||||||
|
options &&
|
||||||
|
options && [
|
||||||
|
...options,
|
||||||
|
{
|
||||||
|
value: "none",
|
||||||
|
query: "none",
|
||||||
|
content: <div className="flex items-center gap-2">None</div>,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
maxHeight="md"
|
||||||
|
position="right"
|
||||||
|
width="w-full"
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -63,7 +63,11 @@ export const PublishProjectModal: React.FC<Props> = observer(() => {
|
|||||||
const [isUnpublishing, setIsUnpublishing] = useState(false);
|
const [isUnpublishing, setIsUnpublishing] = useState(false);
|
||||||
const [isUpdateRequired, setIsUpdateRequired] = useState(false);
|
const [isUpdateRequired, setIsUpdateRequired] = useState(false);
|
||||||
|
|
||||||
const plane_deploy_url = process.env.NEXT_PUBLIC_DEPLOY_URL ?? "http://localhost:4000";
|
let plane_deploy_url = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && !plane_deploy_url) {
|
||||||
|
plane_deploy_url= window.location.protocol + "//" + window.location.host + "/spaces";
|
||||||
|
}
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import SettingsNavbar from "layouts/settings-navbar";
|
|
||||||
|
|
||||||
export const SettingsHeader = () => (
|
|
||||||
<div className="mb-8 space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-2xl font-semibold">Project Settings</h3>
|
|
||||||
<p className="mt-1 text-sm text-custom-text-200">
|
|
||||||
This information will be displayed to every member of the project.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SettingsNavbar />
|
|
||||||
</div>
|
|
||||||
);
|
|
72
web/components/project/settings-sidebar.tsx
Normal file
72
web/components/project/settings-sidebar.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const SettingsSidebar = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const projectLinks: Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
label: "General",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Members",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/members`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Features",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/features`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "States",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/states`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Labels",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Integrations",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/integrations`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Estimates",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Automations",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 w-80 px-9">
|
||||||
|
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
|
||||||
|
<div className="flex flex-col gap-1 w-full">
|
||||||
|
{projectLinks.map((link) => (
|
||||||
|
<Link key={link.href} href={link.href}>
|
||||||
|
<a>
|
||||||
|
<div
|
||||||
|
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||||
|
(
|
||||||
|
link.label === "Import"
|
||||||
|
? router.asPath.includes(link.href)
|
||||||
|
: router.asPath === link.href
|
||||||
|
)
|
||||||
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -30,8 +30,7 @@ const integrationDetails: { [key: string]: any } = {
|
|||||||
},
|
},
|
||||||
slack: {
|
slack: {
|
||||||
logo: SlackLogo,
|
logo: SlackLogo,
|
||||||
description:
|
description: "Get regular updates and control which notification you want to receive.",
|
||||||
"Connect your slack channel to this project to get regular updates. Control which notification you want to receive.",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -93,19 +92,19 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{integration && (
|
{integration && (
|
||||||
<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-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="flex items-start gap-4">
|
||||||
<div className="h-12 w-12 flex-shrink-0">
|
<div className="h-10 w-10 flex-shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={integrationDetails[integration.integration_detail.provider].logo}
|
src={integrationDetails[integration.integration_detail.provider].logo}
|
||||||
alt={`${integration.integration_detail.title} Logo`}
|
alt={`${integration.integration_detail.title} Logo`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="flex items-center gap-4 text-xl font-semibold">
|
<h3 className="flex items-center gap-4 text-sm font-medium">
|
||||||
{integration.integration_detail.title}
|
{integration.integration_detail.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
{integrationDetails[integration.integration_detail.provider].description}
|
{integrationDetails[integration.integration_detail.provider].description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,13 +9,10 @@ import stateService from "services/state.service";
|
|||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "components/ui";
|
import { Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { ArrowDownIcon, ArrowUpIcon } from "@heroicons/react/24/outline";
|
||||||
ArrowDownIcon,
|
|
||||||
ArrowUpIcon,
|
|
||||||
PencilSquareIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { StateGroupIcon } from "components/icons";
|
import { StateGroupIcon } from "components/icons";
|
||||||
|
import { Pencil, X } from "lucide-react";
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
import { groupBy, orderArrayBy } from "helpers/array.helper";
|
import { groupBy, orderArrayBy } from "helpers/array.helper";
|
||||||
@ -160,15 +157,15 @@ export const SingleState: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex items-center justify-between gap-2 border-custom-border-200 bg-custom-background-100 p-5 first:rounded-t-[10px] last:rounded-b-[10px]">
|
<div className="group flex items-center justify-between gap-2 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="20px" width="20px" />
|
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||||
<div>
|
<div>
|
||||||
<h6 className="text-sm">{addSpaceIfCamelCase(state.name)}</h6>
|
<h6 className="text-sm font-medium">{addSpaceIfCamelCase(state.name)}</h6>
|
||||||
<p className="text-xs text-custom-text-200">{state.description}</p>
|
<p className="text-xs text-custom-text-200">{state.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="group flex items-center gap-2.5">
|
||||||
{index !== 0 && (
|
{index !== 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -192,37 +189,43 @@ export const SingleState: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="hidden text-xs text-custom-text-200 group-hover:inline-block"
|
className="hidden text-xs text-custom-sidebar-text-400 group-hover:inline-block"
|
||||||
onClick={handleMakeDefault}
|
onClick={handleMakeDefault}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Set as default
|
Mark as default
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<div className=" items-center gap-2.5 hidden group-hover:flex">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center group-hover:opacity-100 opacity-0"
|
||||||
|
onClick={handleEditState}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button type="button" className="grid place-items-center" onClick={handleEditState}>
|
<button
|
||||||
<PencilSquareIcon className="h-4 w-4 text-custom-text-200" />
|
type="button"
|
||||||
</button>
|
className={`group-hover:opacity-100 opacity-0 ${
|
||||||
<button
|
state.default || groupLength === 1 ? "cursor-not-allowed" : ""
|
||||||
type="button"
|
} grid place-items-center`}
|
||||||
className={`${
|
onClick={handleDeleteState}
|
||||||
state.default || groupLength === 1 ? "cursor-not-allowed" : ""
|
disabled={state.default || groupLength === 1}
|
||||||
} grid place-items-center`}
|
>
|
||||||
onClick={handleDeleteState}
|
{state.default ? (
|
||||||
disabled={state.default || groupLength === 1}
|
<Tooltip tooltipContent="Cannot delete the default state.">
|
||||||
>
|
<X className="h-3.5 w-3.5 text-red-500" />
|
||||||
{state.default ? (
|
</Tooltip>
|
||||||
<Tooltip tooltipContent="Cannot delete the default state.">
|
) : groupLength === 1 ? (
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
<Tooltip tooltipContent="Cannot have an empty group.">
|
||||||
</Tooltip>
|
<X className="h-3.5 w-3.5 text-red-500" />
|
||||||
) : groupLength === 1 ? (
|
</Tooltip>
|
||||||
<Tooltip tooltipContent="Cannot have an empty group.">
|
) : (
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
<X className="h-3.5 w-3.5 text-red-500" />
|
||||||
</Tooltip>
|
)}
|
||||||
) : (
|
</button>
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
</div>
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,8 +6,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
|
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
|
||||||
<div className="flex flex-col items-start gap-3">
|
<div className="flex flex-col items-start gap-3 py-3.5 border-b border-custom-border-200">
|
||||||
<h3 className="text-2xl font-semibold">{bannerName}</h3>
|
<h3 className="text-xl font-medium">{bannerName}</h3>
|
||||||
{description && (
|
{description && (
|
||||||
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">
|
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">
|
||||||
<ExclamationIcon height={24} width={24} className="fill-current text-custom-text-100" />
|
<ExclamationIcon height={24} width={24} className="fill-current text-custom-text-100" />
|
||||||
|
@ -18,24 +18,24 @@ export const ToggleSwitch: React.FC<Props> = (props) => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className={`relative flex-shrink-0 inline-flex ${
|
className={`relative flex-shrink-0 inline-flex ${
|
||||||
size === "sm" ? "h-3.5 w-6" : size === "md" ? "h-4 w-7" : "h-6 w-11"
|
size === "sm" ? "h-4 w-6" : size === "md" ? "h-5 w-8" : "h-6 w-10"
|
||||||
} flex-shrink-0 cursor-pointer rounded-full border-2 border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
|
} flex-shrink-0 cursor-pointer rounded-full border border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
value ? "bg-green-500" : "bg-custom-background-80"
|
value ? "bg-custom-primary-100" : "bg-gray-700"
|
||||||
} ${className || ""}`}
|
} ${className || ""}`}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{label}</span>
|
<span className="sr-only">{label}</span>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`inline-block ${
|
className={`self-center inline-block ${
|
||||||
size === "sm" ? "h-2.5 w-2.5" : size === "md" ? "h-3 w-3" : "h-5 w-5"
|
size === "sm" ? "h-2 w-2" : size === "md" ? "h-3 w-3" : "h-4 w-4"
|
||||||
} transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${
|
} transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
value
|
value
|
||||||
? (size === "sm"
|
? (size === "sm"
|
||||||
? "translate-x-2.5"
|
|
||||||
: size === "md"
|
|
||||||
? "translate-x-3"
|
? "translate-x-3"
|
||||||
|
: size === "md"
|
||||||
|
? "translate-x-4"
|
||||||
: "translate-x-5") + " bg-white"
|
: "translate-x-5") + " bg-white"
|
||||||
: "translate-x-0 bg-custom-background-90"
|
: "translate-x-1 bg-custom-background-90"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
@ -9,13 +7,13 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
|
|||||||
import useProjectDetails from "hooks/use-project-details";
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
// components
|
// components
|
||||||
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||||
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
|
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {};
|
type Props = { disableUserActions: boolean };
|
||||||
|
|
||||||
export const ViewIssuesGanttChartView: FC<Props> = ({}) => {
|
export const ViewIssuesGanttChartView: React.FC<Props> = ({ disableUserActions }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
@ -31,22 +29,30 @@ export const ViewIssuesGanttChartView: FC<Props> = ({}) => {
|
|||||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<>
|
||||||
<GanttChartRoot
|
<IssuePeekOverview
|
||||||
border={false}
|
handleMutation={() => mutateGanttIssues()}
|
||||||
title="Issues"
|
projectId={projectId?.toString() ?? ""}
|
||||||
loaderTitle="Issues"
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
|
readOnly={disableUserActions}
|
||||||
blockUpdateHandler={(block, payload) =>
|
|
||||||
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
|
||||||
}
|
|
||||||
SidebarBlockRender={IssueGanttSidebarBlock}
|
|
||||||
BlockRender={IssueGanttBlock}
|
|
||||||
enableBlockLeftResize={isAllowed}
|
|
||||||
enableBlockRightResize={isAllowed}
|
|
||||||
enableBlockMove={isAllowed}
|
|
||||||
enableReorder={isAllowed}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<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={isAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,9 +4,21 @@ import React, { useState } from "react";
|
|||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// swr
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// react hook forms
|
// react hook forms
|
||||||
import { Control, Controller, useWatch } from "react-hook-form";
|
import { Control, Controller, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
|
||||||
|
// fetch keys
|
||||||
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
import { BlockedIcon, BlockerIcon } from "components/icons";
|
||||||
import { ChevronDown, PlayIcon, User, X, CalendarDays, LayoutGrid, Users } from "lucide-react";
|
import { ChevronDown, PlayIcon, User, X, CalendarDays, LayoutGrid, Users } from "lucide-react";
|
||||||
@ -26,6 +38,7 @@ import {
|
|||||||
EstimateSelect,
|
EstimateSelect,
|
||||||
ParentSelect,
|
ParentSelect,
|
||||||
BlockerSelect,
|
BlockerSelect,
|
||||||
|
BlockedSelect,
|
||||||
} from "components/web-view";
|
} from "components/web-view";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
@ -39,15 +52,16 @@ type Props = {
|
|||||||
export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||||
const { control, submitChanges } = props;
|
const { control, submitChanges } = props;
|
||||||
|
|
||||||
const blockerIssue = useWatch({
|
const blockerIssue =
|
||||||
control,
|
useWatch({
|
||||||
name: "blocker_issues",
|
control,
|
||||||
});
|
name: "issue_relations",
|
||||||
|
})?.filter((i) => i.relation_type === "blocked_by") || [];
|
||||||
|
|
||||||
const blockedIssue = useWatch({
|
const blockedIssue = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "blocked_issues",
|
name: "related_issues",
|
||||||
});
|
})?.filter((i) => i.relation_type === "blocked_by");
|
||||||
|
|
||||||
const startDate = useWatch({
|
const startDate = useWatch({
|
||||||
control,
|
control,
|
||||||
@ -55,12 +69,28 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const [isViewAllOpen, setIsViewAllOpen] = useState(false);
|
const [isViewAllOpen, setIsViewAllOpen] = useState(false);
|
||||||
|
|
||||||
const { isEstimateActive } = useEstimateOption();
|
const { isEstimateActive } = useEstimateOption();
|
||||||
|
|
||||||
|
const handleMutation = (data: any) => {
|
||||||
|
mutate<IIssue>(
|
||||||
|
ISSUE_DETAILS(issueId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Label>Details</Label>
|
<Label>Details</Label>
|
||||||
@ -188,51 +218,80 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
|||||||
<span className="text-sm text-custom-text-400">Blocking</span>
|
<span className="text-sm text-custom-text-400">Blocking</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Controller
|
<BlockerSelect
|
||||||
control={control}
|
value={null}
|
||||||
name="blocker_issues"
|
onChange={(val) => {
|
||||||
render={({ field: { value } }) => (
|
if (!user || !workspaceSlug || !projectId || !issueId) return;
|
||||||
<BlockerSelect
|
|
||||||
value={value}
|
issuesService
|
||||||
onChange={(val) =>
|
.createIssueRelation(
|
||||||
submitChanges({
|
workspaceSlug as string,
|
||||||
blocker_issues: val,
|
projectId as string,
|
||||||
blockers_list: val?.map((i: any) => i.blocker_issue_detail?.id ?? ""),
|
issueId as string,
|
||||||
})
|
user,
|
||||||
}
|
{
|
||||||
/>
|
related_list: [
|
||||||
)}
|
...val.map((issue: any) => ({
|
||||||
|
issue: issue.blocker_issue_detail.id,
|
||||||
|
relation_type: "blocked_by" as const,
|
||||||
|
related_issue: issueId as string,
|
||||||
|
related_issue_detail: issue.blocker_issue_detail,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
handleMutation({
|
||||||
|
issue_relations: [
|
||||||
|
...blockerIssue,
|
||||||
|
...(response ?? []).map((i: any) => ({
|
||||||
|
id: i.id,
|
||||||
|
relation_type: i.relation_type,
|
||||||
|
issue_detail: i.related_issue_detail,
|
||||||
|
issue: i.related_issue,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{blockerIssue &&
|
{blockerIssue &&
|
||||||
blockerIssue.map((issue) => (
|
blockerIssue.map((issue) => (
|
||||||
<div
|
<div
|
||||||
key={issue.blocker_issue_detail?.id}
|
key={issue.issue_detail?.id}
|
||||||
className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${issue.blocker_issue_detail?.project_detail.id}/issues/${issue.blocker_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${issue.issue_detail?.project_detail.id}/issues/${issue.issue_detail?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<BlockerIcon height={10} width={10} />
|
<BlockerIcon height={10} width={10} />
|
||||||
{`${issue.blocker_issue_detail?.project_detail.identifier}-${issue.blocker_issue_detail?.sequence_id}`}
|
{`${issue.issue_detail?.project_detail.identifier}-${issue.issue_detail?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="duration-300"
|
className="duration-300"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedBlockers = blockerIssue.filter(
|
const updatedBlockers = blockerIssue.filter(
|
||||||
(i) => i.blocker_issue_detail?.id !== issue.blocker_issue_detail?.id
|
(i) => i.issue_detail?.id !== issue.issue_detail?.id
|
||||||
);
|
);
|
||||||
|
|
||||||
submitChanges({
|
if (!user) return;
|
||||||
blocker_issues: updatedBlockers,
|
|
||||||
blockers_list: updatedBlockers.map(
|
issuesService.deleteIssueRelation(
|
||||||
(i) => i.blocker_issue_detail?.id ?? ""
|
workspaceSlug as string,
|
||||||
),
|
projectId as string,
|
||||||
|
issueId as string,
|
||||||
|
issue.id,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
handleMutation({
|
||||||
|
issue_relations: updatedBlockers,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -250,49 +309,80 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
|||||||
<span className="text-sm text-custom-text-400">Blocked by</span>
|
<span className="text-sm text-custom-text-400">Blocked by</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Controller
|
<BlockedSelect
|
||||||
control={control}
|
value={null}
|
||||||
name="blocked_issues"
|
onChange={(val) => {
|
||||||
render={({ field: { value } }) => (
|
if (!user || !workspaceSlug || !projectId || !issueId) return;
|
||||||
<BlockerSelect
|
|
||||||
value={value}
|
issuesService
|
||||||
onChange={(val) =>
|
.createIssueRelation(
|
||||||
submitChanges({
|
workspaceSlug as string,
|
||||||
blocked_issues: val,
|
projectId as string,
|
||||||
blocks_list: val?.map((i: any) => i.blocker_issue_detail?.id ?? ""),
|
issueId as string,
|
||||||
})
|
user,
|
||||||
}
|
{
|
||||||
/>
|
related_list: [
|
||||||
)}
|
...val.map((issue: any) => ({
|
||||||
|
issue: issue.blocked_issue_detail.id,
|
||||||
|
relation_type: "blocked_by" as const,
|
||||||
|
related_issue: issueId as string,
|
||||||
|
related_issue_detail: issue.blocked_issue_detail,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
handleMutation({
|
||||||
|
related_issues: [
|
||||||
|
...blockedIssue,
|
||||||
|
...(response ?? []).map((i: any) => ({
|
||||||
|
id: i.id,
|
||||||
|
relation_type: i.relation_type,
|
||||||
|
issue_detail: i.related_issue_detail,
|
||||||
|
issue: i.related_issue,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{blockedIssue &&
|
{blockedIssue &&
|
||||||
blockedIssue.map((issue) => (
|
blockedIssue.map((issue) => (
|
||||||
<div
|
<div
|
||||||
key={issue.blocked_issue_detail?.id}
|
key={issue.related_issue_detail?.id}
|
||||||
className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${issue.blocked_issue_detail?.project_detail.id}/issues/${issue.blocked_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${issue.related_issue_detail?.project_detail.id}/issues/${issue.related_issue_detail?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<BlockedIcon height={10} width={10} />
|
<BlockedIcon height={10} width={10} />
|
||||||
{`${issue?.blocked_issue_detail?.project_detail?.identifier}-${issue?.blocked_issue_detail?.sequence_id}`}
|
{`${issue?.related_issue_detail?.project_detail?.identifier}-${issue?.related_issue_detail?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="duration-300"
|
className="duration-300"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedBlocked = blockedIssue.filter(
|
const updatedBlocked = blockedIssue.filter(
|
||||||
(i) => i.blocked_issue_detail?.id !== issue.blocked_issue_detail?.id
|
(i) => i.related_issue_detail?.id !== issue.related_issue_detail?.id
|
||||||
);
|
);
|
||||||
|
|
||||||
submitChanges({
|
if (!user) return;
|
||||||
blocked_issues: updatedBlocked,
|
|
||||||
blocks_list: updatedBlocked.map((i) => i.blocked_issue_detail?.id ?? ""),
|
issuesService.deleteIssueRelation(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
issueId as string,
|
||||||
|
issue.id,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
handleMutation({
|
||||||
|
related_issues: updatedBlocked,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -66,7 +66,7 @@ export const BlockedSelect: React.FC<Props> = (props) => {
|
|||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
isOpen={isBlockedModalOpen}
|
isOpen={isBlockedModalOpen}
|
||||||
handleClose={() => setIsBlockedModalOpen(false)}
|
handleClose={() => setIsBlockedModalOpen(false)}
|
||||||
searchParams={{ blocker_blocked_by: true, issue_id: issueId!.toString() }}
|
searchParams={{ issue_relation: true, issue_id: issueId!.toString() }}
|
||||||
handleOnSubmit={onSubmit}
|
handleOnSubmit={onSubmit}
|
||||||
workspaceLevelToggle
|
workspaceLevelToggle
|
||||||
/>
|
/>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user