Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1604f4585d | ||
|
|
03f9ca45d8 |
@@ -11,7 +11,9 @@ from .workspace import (
|
||||
)
|
||||
from .project import (
|
||||
ProjectSerializer,
|
||||
ProjectSettingSerializer,
|
||||
ProjectDetailSerializer,
|
||||
ProjectSettingDetailSerializer,
|
||||
ProjectMemberSerializer,
|
||||
ProjectMemberInviteSerializer,
|
||||
ProjectIdentifierSerializer,
|
||||
@@ -20,7 +22,7 @@ from .project import (
|
||||
ProjectMemberLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer
|
||||
ProjectPublicMemberSerializer,
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
|
||||
@@ -10,6 +10,7 @@ from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSe
|
||||
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectSetting,
|
||||
ProjectMember,
|
||||
ProjectMemberInvite,
|
||||
ProjectIdentifier,
|
||||
@@ -96,8 +97,6 @@ class ProjectLiteSerializer(BaseSerializer):
|
||||
|
||||
class ProjectDetailSerializer(BaseSerializer):
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
default_assignee = UserLiteSerializer(read_only=True)
|
||||
project_lead = UserLiteSerializer(read_only=True)
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_cycles = serializers.IntegerField(read_only=True)
|
||||
@@ -178,12 +177,12 @@ class ProjectDeployBoardSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project", "anchor",
|
||||
"project",
|
||||
"anchor",
|
||||
]
|
||||
|
||||
|
||||
class ProjectPublicMemberSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProjectPublicMember
|
||||
fields = "__all__"
|
||||
@@ -192,3 +191,26 @@ class ProjectPublicMemberSerializer(BaseSerializer):
|
||||
"project",
|
||||
"member",
|
||||
]
|
||||
|
||||
|
||||
class ProjectSettingSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = ProjectSetting
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
|
||||
class ProjectSettingDetailSerializer(BaseSerializer):
|
||||
default_assignee = UserLiteSerializer(read_only=True)
|
||||
project_lead = UserLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectSetting
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
||||
from .analytic import urlpatterns as analytic_urls
|
||||
from .asset import urlpatterns as asset_urls
|
||||
from .authentication import urlpatterns as authentication_urls
|
||||
from .configuration import urlpatterns as configuration_urls
|
||||
from .cycle import urlpatterns as cycle_urls
|
||||
from .estimate import urlpatterns as estimate_urls
|
||||
from .gpt import urlpatterns as gpt_urls
|
||||
from .importer import urlpatterns as importer_urls
|
||||
from .inbox import urlpatterns as inbox_urls
|
||||
from .integration import urlpatterns as integration_urls
|
||||
from .issue import urlpatterns as issue_urls
|
||||
from .module import urlpatterns as module_urls
|
||||
from .notification import urlpatterns as notification_urls
|
||||
from .page import urlpatterns as page_urls
|
||||
from .project import urlpatterns as project_urls
|
||||
from .public_board import urlpatterns as public_board_urls
|
||||
from .release_note import urlpatterns as release_note_urls
|
||||
from .search import urlpatterns as search_urls
|
||||
from .state import urlpatterns as state_urls
|
||||
from .unsplash import urlpatterns as unsplash_urls
|
||||
from .user import urlpatterns as user_urls
|
||||
from .views import urlpatterns as view_urls
|
||||
from .workspace import urlpatterns as workspace_urls
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
*analytic_urls,
|
||||
*asset_urls,
|
||||
*authentication_urls,
|
||||
*configuration_urls,
|
||||
*cycle_urls,
|
||||
*estimate_urls,
|
||||
*gpt_urls,
|
||||
*importer_urls,
|
||||
*inbox_urls,
|
||||
*integration_urls,
|
||||
*issue_urls,
|
||||
*module_urls,
|
||||
*notification_urls,
|
||||
*page_urls,
|
||||
*project_urls,
|
||||
*public_board_urls,
|
||||
*release_note_urls,
|
||||
*search_urls,
|
||||
*state_urls,
|
||||
*unsplash_urls,
|
||||
*user_urls,
|
||||
*view_urls,
|
||||
*workspace_urls,
|
||||
]
|
||||
@@ -1,46 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
AnalyticsEndpoint,
|
||||
AnalyticViewViewset,
|
||||
SavedAnalyticEndpoint,
|
||||
ExportAnalyticsEndpoint,
|
||||
DefaultAnalyticsEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/analytics/",
|
||||
AnalyticsEndpoint.as_view(),
|
||||
name="plane-analytics",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/analytic-view/",
|
||||
AnalyticViewViewset.as_view({"get": "list", "post": "create"}),
|
||||
name="analytic-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/analytic-view/<uuid:pk>/",
|
||||
AnalyticViewViewset.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
name="analytic-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/saved-analytic-view/<uuid:analytic_id>/",
|
||||
SavedAnalyticEndpoint.as_view(),
|
||||
name="saved-analytic-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/export-analytics/",
|
||||
ExportAnalyticsEndpoint.as_view(),
|
||||
name="export-analytics",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/default-analytics/",
|
||||
DefaultAnalyticsEndpoint.as_view(),
|
||||
name="default-analytics",
|
||||
),
|
||||
]
|
||||
@@ -1,31 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
FileAssetEndpoint,
|
||||
UserAssetsEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/file-assets/",
|
||||
FileAssetEndpoint.as_view(),
|
||||
name="file-assets",
|
||||
),
|
||||
path(
|
||||
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/",
|
||||
FileAssetEndpoint.as_view(),
|
||||
name="file-assets",
|
||||
),
|
||||
path(
|
||||
"users/file-assets/",
|
||||
UserAssetsEndpoint.as_view(),
|
||||
name="user-file-assets",
|
||||
),
|
||||
path(
|
||||
"users/file-assets/<str:asset_key>/",
|
||||
UserAssetsEndpoint.as_view(),
|
||||
name="user-file-assets",
|
||||
),
|
||||
]
|
||||
@@ -1,60 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
# Authentication
|
||||
SignUpEndpoint,
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
MagicSignInGenerateEndpoint,
|
||||
OauthEndpoint,
|
||||
## End Authentication
|
||||
# Auth Extended
|
||||
ForgotPasswordEndpoint,
|
||||
VerifyEmailEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
RequestEmailVerificationEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
## End Auth Extender
|
||||
# API Tokens
|
||||
ApiTokenEndpoint,
|
||||
## End API Tokens
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# Social Auth
|
||||
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
||||
# Auth
|
||||
path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"),
|
||||
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||
# Magic Sign In/Up
|
||||
path(
|
||||
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
||||
),
|
||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||
# Email verification
|
||||
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||
path(
|
||||
"request-email-verify/",
|
||||
RequestEmailVerificationEndpoint.as_view(),
|
||||
name="request-reset-email",
|
||||
),
|
||||
# Password Manipulation
|
||||
path(
|
||||
"reset-password/<uidb64>/<token>/",
|
||||
ResetPasswordEndpoint.as_view(),
|
||||
name="password-reset",
|
||||
),
|
||||
path(
|
||||
"forgot-password/",
|
||||
ForgotPasswordEndpoint.as_view(),
|
||||
name="forgot-password",
|
||||
),
|
||||
# API Tokens
|
||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||
## End API Tokens
|
||||
]
|
||||
@@ -1,12 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import ConfigurationEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"configs/",
|
||||
ConfigurationEndpoint.as_view(),
|
||||
name="configuration",
|
||||
),
|
||||
]
|
||||
@@ -1,87 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
CycleViewSet,
|
||||
CycleIssueViewSet,
|
||||
CycleDateCheckEndpoint,
|
||||
CycleFavoriteViewSet,
|
||||
TransferCycleIssueEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
|
||||
CycleViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/",
|
||||
CycleViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
|
||||
CycleIssueViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
|
||||
CycleIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/date-check/",
|
||||
CycleDateCheckEndpoint.as_view(),
|
||||
name="project-cycle-date",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
|
||||
CycleFavoriteViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="user-favorite-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
|
||||
CycleFavoriteViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="user-favorite-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
|
||||
TransferCycleIssueEndpoint.as_view(),
|
||||
name="transfer-issues",
|
||||
),
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
ProjectEstimatePointEndpoint,
|
||||
BulkEstimatePointEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
|
||||
ProjectEstimatePointEndpoint.as_view(),
|
||||
name="project-estimate-points",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||
BulkEstimatePointEndpoint.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="bulk-create-estimate-points",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
|
||||
BulkEstimatePointEndpoint.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="bulk-create-estimate-points",
|
||||
),
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import GPTIntegrationEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
|
||||
GPTIntegrationEndpoint.as_view(),
|
||||
name="importer",
|
||||
),
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
ServiceIssueImportSummaryEndpoint,
|
||||
ImportServiceEndpoint,
|
||||
UpdateServiceImportStatusEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/importers/<str:service>/",
|
||||
ServiceIssueImportSummaryEndpoint.as_view(),
|
||||
name="importer-summary",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/importers/<str:service>/",
|
||||
ImportServiceEndpoint.as_view(),
|
||||
name="importer",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/importers/",
|
||||
ImportServiceEndpoint.as_view(),
|
||||
name="importer",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/importers/<str:service>/<uuid:pk>/",
|
||||
ImportServiceEndpoint.as_view(),
|
||||
name="importer",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/service/<str:service>/importers/<uuid:importer_id>/",
|
||||
UpdateServiceImportStatusEndpoint.as_view(),
|
||||
name="importer-status",
|
||||
),
|
||||
]
|
||||
@@ -1,53 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
InboxViewSet,
|
||||
InboxIssueViewSet,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
|
||||
InboxViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="inbox",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
|
||||
InboxViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="inbox",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||
InboxIssueViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||
InboxIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
]
|
||||
@@ -1,150 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
IntegrationViewSet,
|
||||
WorkspaceIntegrationViewSet,
|
||||
GithubRepositoriesEndpoint,
|
||||
GithubRepositorySyncViewSet,
|
||||
GithubIssueSyncViewSet,
|
||||
GithubCommentSyncViewSet,
|
||||
BulkCreateGithubIssueSyncEndpoint,
|
||||
SlackProjectSyncViewSet,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"integrations/",
|
||||
IntegrationViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="integrations",
|
||||
),
|
||||
path(
|
||||
"integrations/<uuid:pk>/",
|
||||
IntegrationViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="integrations",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-integrations/",
|
||||
WorkspaceIntegrationViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
name="workspace-integrations",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-integrations/<str:provider>/",
|
||||
WorkspaceIntegrationViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="workspace-integrations",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-integrations/<uuid:pk>/provider/",
|
||||
WorkspaceIntegrationViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="workspace-integrations",
|
||||
),
|
||||
# Github Integrations
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-integrations/<uuid:workspace_integration_id>/github-repositories/",
|
||||
GithubRepositoriesEndpoint.as_view(),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/github-repository-sync/",
|
||||
GithubRepositorySyncViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/github-repository-sync/<uuid:pk>/",
|
||||
GithubRepositorySyncViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/",
|
||||
GithubIssueSyncViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/bulk-create-github-issue-sync/",
|
||||
BulkCreateGithubIssueSyncEndpoint.as_view(),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:pk>/",
|
||||
GithubIssueSyncViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:issue_sync_id>/github-comment-sync/",
|
||||
GithubCommentSyncViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:issue_sync_id>/github-comment-sync/<uuid:pk>/",
|
||||
GithubCommentSyncViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
),
|
||||
## End Github Integrations
|
||||
# Slack Integration
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
|
||||
SlackProjectSyncViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
|
||||
SlackProjectSyncViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
}
|
||||
),
|
||||
),
|
||||
## End Slack Integration
|
||||
]
|
||||
@@ -1,332 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
IssueViewSet,
|
||||
LabelViewSet,
|
||||
BulkCreateIssueLabelsEndpoint,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
BulkImportIssuesEndpoint,
|
||||
UserWorkSpaceIssues,
|
||||
SubIssuesEndpoint,
|
||||
IssueLinkViewSet,
|
||||
IssueAttachmentEndpoint,
|
||||
ExportIssuesEndpoint,
|
||||
IssueActivityEndpoint,
|
||||
IssueCommentViewSet,
|
||||
IssueSubscriberViewSet,
|
||||
IssueReactionViewSet,
|
||||
CommentReactionViewSet,
|
||||
IssuePropertyViewSet,
|
||||
IssueArchiveViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueDraftViewSet,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
|
||||
IssueViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
|
||||
IssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||
LabelViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-labels",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/<uuid:pk>/",
|
||||
LabelViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-labels",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-create-labels/",
|
||||
BulkCreateIssueLabelsEndpoint.as_view(),
|
||||
name="project-bulk-labels",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-delete-issues/",
|
||||
BulkDeleteIssuesEndpoint.as_view(),
|
||||
name="project-issues-bulk",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-import-issues/<str:service>/",
|
||||
BulkImportIssuesEndpoint.as_view(),
|
||||
name="project-issues-bulk",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/my-issues/",
|
||||
UserWorkSpaceIssues.as_view(),
|
||||
name="workspace-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
|
||||
SubIssuesEndpoint.as_view(),
|
||||
name="sub-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
|
||||
IssueLinkViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-links",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/<uuid:pk>/",
|
||||
IssueLinkViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-links",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
|
||||
IssueAttachmentEndpoint.as_view(),
|
||||
name="project-issue-attachments",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
|
||||
IssueAttachmentEndpoint.as_view(),
|
||||
name="project-issue-attachments",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/export-issues/",
|
||||
ExportIssuesEndpoint.as_view(),
|
||||
name="export-issues",
|
||||
),
|
||||
## End Issues
|
||||
## Issue Activity
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/history/",
|
||||
IssueActivityEndpoint.as_view(),
|
||||
name="project-issue-history",
|
||||
),
|
||||
## Issue Activity
|
||||
## IssueComments
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
|
||||
IssueCommentViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-comment",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||
IssueCommentViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-comment",
|
||||
),
|
||||
## End IssueComments
|
||||
# Issue Subscribers
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
|
||||
IssueSubscriberViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-subscribers",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
|
||||
IssueSubscriberViewSet.as_view({"delete": "destroy"}),
|
||||
name="project-issue-subscribers",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
|
||||
IssueSubscriberViewSet.as_view(
|
||||
{
|
||||
"get": "subscription_status",
|
||||
"post": "subscribe",
|
||||
"delete": "unsubscribe",
|
||||
}
|
||||
),
|
||||
name="project-issue-subscribers",
|
||||
),
|
||||
## End Issue Subscribers
|
||||
# Issue Reactions
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
|
||||
IssueReactionViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-reactions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
|
||||
IssueReactionViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-reactions",
|
||||
),
|
||||
## End Issue Reactions
|
||||
# Comment Reactions
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
|
||||
CommentReactionViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-comment-reactions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
|
||||
CommentReactionViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-comment-reactions",
|
||||
),
|
||||
## End Comment Reactions
|
||||
## IssueProperty
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
|
||||
IssuePropertyViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-roadmap",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/<uuid:pk>/",
|
||||
IssuePropertyViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-roadmap",
|
||||
),
|
||||
## IssueProperty Ebd
|
||||
## Issue Archives
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
|
||||
IssueArchiveViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
name="project-issue-archive",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/",
|
||||
IssueArchiveViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-archive",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/unarchive/<uuid:pk>/",
|
||||
IssueArchiveViewSet.as_view(
|
||||
{
|
||||
"post": "unarchive",
|
||||
}
|
||||
),
|
||||
name="project-issue-archive",
|
||||
),
|
||||
## End Issue Archives
|
||||
## Issue Relation
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||
IssueRelationViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
||||
IssueRelationViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
),
|
||||
## End Issue Relation
|
||||
## Issue Drafts
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
|
||||
IssueDraftViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-draft",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
|
||||
IssueDraftViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-draft",
|
||||
),
|
||||
]
|
||||
@@ -1,104 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
ModuleViewSet,
|
||||
ModuleIssueViewSet,
|
||||
ModuleLinkViewSet,
|
||||
ModuleFavoriteViewSet,
|
||||
BulkImportModulesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/",
|
||||
ModuleViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-modules",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:pk>/",
|
||||
ModuleViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-modules",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
|
||||
ModuleIssueViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-module-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
|
||||
ModuleIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-module-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
|
||||
ModuleLinkViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-module-links",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/<uuid:pk>/",
|
||||
ModuleLinkViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-module-links",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
|
||||
ModuleFavoriteViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="user-favorite-module",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
|
||||
ModuleFavoriteViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="user-favorite-module",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-import-modules/<str:service>/",
|
||||
BulkImportModulesEndpoint.as_view(),
|
||||
name="bulk-modules-create",
|
||||
),
|
||||
]
|
||||
@@ -1,66 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
NotificationViewSet,
|
||||
UnreadNotificationEndpoint,
|
||||
MarkAllReadNotificationViewSet,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/",
|
||||
NotificationViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
name="notifications",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
|
||||
NotificationViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="notifications",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
|
||||
NotificationViewSet.as_view(
|
||||
{
|
||||
"post": "mark_read",
|
||||
"delete": "mark_unread",
|
||||
}
|
||||
),
|
||||
name="notifications",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
|
||||
NotificationViewSet.as_view(
|
||||
{
|
||||
"post": "archive",
|
||||
"delete": "unarchive",
|
||||
}
|
||||
),
|
||||
name="notifications",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/unread/",
|
||||
UnreadNotificationEndpoint.as_view(),
|
||||
name="unread-notifications",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/mark-all-read/",
|
||||
MarkAllReadNotificationViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="mark-all-read-notifications",
|
||||
),
|
||||
]
|
||||
@@ -1,79 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
PageViewSet,
|
||||
PageBlockViewSet,
|
||||
PageFavoriteViewSet,
|
||||
CreateIssueFromPageBlockEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
|
||||
PageViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-pages",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
|
||||
PageViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-pages",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/",
|
||||
PageBlockViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-page-blocks",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:pk>/",
|
||||
PageBlockViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-page-blocks",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
|
||||
PageFavoriteViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="user-favorite-pages",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/<uuid:page_id>/",
|
||||
PageFavoriteViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="user-favorite-pages",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
|
||||
CreateIssueFromPageBlockEndpoint.as_view(),
|
||||
name="page-block-issues",
|
||||
),
|
||||
]
|
||||
@@ -1,143 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import (
|
||||
ProjectViewSet,
|
||||
InviteProjectEndpoint,
|
||||
ProjectMemberViewSet,
|
||||
ProjectMemberEndpoint,
|
||||
ProjectMemberInvitationsViewset,
|
||||
ProjectMemberUserEndpoint,
|
||||
AddMemberToProjectEndpoint,
|
||||
ProjectJoinEndpoint,
|
||||
AddTeamToProjectEndpoint,
|
||||
ProjectUserViewsEndpoint,
|
||||
ProjectIdentifierEndpoint,
|
||||
ProjectFavoritesViewSet,
|
||||
LeaveProjectEndpoint,
|
||||
ProjectPublicCoverImagesEndpoint
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/",
|
||||
ProjectViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:pk>/",
|
||||
ProjectViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-identifiers/",
|
||||
ProjectIdentifierEndpoint.as_view(),
|
||||
name="project-identifiers",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/invite/",
|
||||
InviteProjectEndpoint.as_view(),
|
||||
name="invite-project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
|
||||
ProjectMemberViewSet.as_view({"get": "list"}),
|
||||
name="project-member",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
|
||||
ProjectMemberViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-member",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
|
||||
ProjectMemberEndpoint.as_view(),
|
||||
name="project-member",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/add/",
|
||||
AddMemberToProjectEndpoint.as_view(),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/join/",
|
||||
ProjectJoinEndpoint.as_view(),
|
||||
name="project-join",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
|
||||
AddTeamToProjectEndpoint.as_view(),
|
||||
name="projects",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
|
||||
ProjectMemberInvitationsViewset.as_view({"get": "list"}),
|
||||
name="project-member-invite",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
|
||||
ProjectMemberInvitationsViewset.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-member-invite",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
|
||||
ProjectUserViewsEndpoint.as_view(),
|
||||
name="project-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/me/",
|
||||
ProjectMemberUserEndpoint.as_view(),
|
||||
name="project-member-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-favorite-projects/",
|
||||
ProjectFavoritesViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-favorite",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
|
||||
ProjectFavoritesViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-favorite",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
|
||||
LeaveProjectEndpoint.as_view(),
|
||||
name="leave-project",
|
||||
),
|
||||
path(
|
||||
"project-covers/",
|
||||
ProjectPublicCoverImagesEndpoint.as_view(),
|
||||
name="project-covers",
|
||||
),
|
||||
]
|
||||
@@ -1,151 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
ProjectDeployBoardViewSet,
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
IssueRetrievePublicEndpoint,
|
||||
IssueCommentPublicViewSet,
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
InboxIssuePublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
|
||||
ProjectDeployBoardViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
|
||||
ProjectDeployBoardViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
|
||||
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
|
||||
name="project-deploy-board-settings",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
|
||||
ProjectIssuesPublicEndpoint.as_view(),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
|
||||
IssueRetrievePublicEndpoint.as_view(),
|
||||
name="workspace-project-boards",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
|
||||
IssueCommentPublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="issue-comments-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||
IssueCommentPublicViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-comments-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
|
||||
IssueReactionPublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="issue-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
|
||||
IssueReactionPublicViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
|
||||
CommentReactionPublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="comment-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
|
||||
CommentReactionPublicViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="comment-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||
InboxIssuePublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||
InboxIssuePublicViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
|
||||
IssueVotePublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-vote-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/",
|
||||
WorkspaceProjectDeployBoardEndpoint.as_view(),
|
||||
name="workspace-project-boards",
|
||||
),
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import ReleaseNotesEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"release-notes/",
|
||||
ReleaseNotesEndpoint.as_view(),
|
||||
name="release-notes",
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
GlobalSearchEndpoint,
|
||||
IssueSearchEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/search/",
|
||||
GlobalSearchEndpoint.as_view(),
|
||||
name="global-search",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/search-issues/",
|
||||
IssueSearchEndpoint.as_view(),
|
||||
name="project-issue-search",
|
||||
),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import StateViewSet
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/states/",
|
||||
StateViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-states",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
|
||||
StateViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-state",
|
||||
),
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import UnsplashEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"unsplash/",
|
||||
UnsplashEndpoint.as_view(),
|
||||
name="unsplash",
|
||||
),
|
||||
]
|
||||
@@ -1,104 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import (
|
||||
## User
|
||||
UserEndpoint,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
## End User
|
||||
## Workspaces
|
||||
UserWorkspaceInvitationsEndpoint,
|
||||
UserWorkSpacesEndpoint,
|
||||
JoinWorkspaceEndpoint,
|
||||
UserWorkspaceInvitationsEndpoint,
|
||||
UserWorkspaceInvitationEndpoint,
|
||||
UserActivityGraphEndpoint,
|
||||
UserIssueCompletedGraphEndpoint,
|
||||
UserWorkspaceDashboardEndpoint,
|
||||
UserProjectInvitationsViewset,
|
||||
## End Workspaces
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# User Profile
|
||||
path(
|
||||
"users/me/",
|
||||
UserEndpoint.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
name="users",
|
||||
),
|
||||
path(
|
||||
"users/me/change-password/",
|
||||
ChangePasswordEndpoint.as_view(),
|
||||
name="change-password",
|
||||
),
|
||||
path(
|
||||
"users/me/onboard/",
|
||||
UpdateUserOnBoardedEndpoint.as_view(),
|
||||
name="user-onboard",
|
||||
),
|
||||
path(
|
||||
"users/me/tour-completed/",
|
||||
UpdateUserTourCompletedEndpoint.as_view(),
|
||||
name="user-tour",
|
||||
),
|
||||
path(
|
||||
"users/workspaces/<str:slug>/activities/",
|
||||
UserActivityEndpoint.as_view(),
|
||||
name="user-activities",
|
||||
),
|
||||
# user workspaces
|
||||
path(
|
||||
"users/me/workspaces/",
|
||||
UserWorkSpacesEndpoint.as_view(),
|
||||
name="user-workspace",
|
||||
),
|
||||
# user workspace invitations
|
||||
path(
|
||||
"users/me/invitations/workspaces/",
|
||||
UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
|
||||
name="user-workspace-invitations",
|
||||
),
|
||||
# user workspace invitation
|
||||
path(
|
||||
"users/me/invitations/<uuid:pk>/",
|
||||
UserWorkspaceInvitationEndpoint.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
}
|
||||
),
|
||||
name="user-workspace-invitation",
|
||||
),
|
||||
# user join workspace
|
||||
# User Graphs
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/activity-graph/",
|
||||
UserActivityGraphEndpoint.as_view(),
|
||||
name="user-activity-graph",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/issues-completed-graph/",
|
||||
UserIssueCompletedGraphEndpoint.as_view(),
|
||||
name="completed-graph",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/dashboard/",
|
||||
UserWorkspaceDashboardEndpoint.as_view(),
|
||||
name="user-workspace-dashboard",
|
||||
),
|
||||
## End User Graph
|
||||
path(
|
||||
"users/me/invitations/workspaces/<str:slug>/<uuid:pk>/join/",
|
||||
JoinWorkspaceEndpoint.as_view(),
|
||||
name="user-join-workspace",
|
||||
),
|
||||
# user project invitations
|
||||
path(
|
||||
"users/me/invitations/projects/",
|
||||
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
|
||||
name="user-project-invitations",
|
||||
),
|
||||
]
|
||||
@@ -1,85 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
IssueViewViewSet,
|
||||
GlobalViewViewSet,
|
||||
GlobalViewIssuesViewSet,
|
||||
IssueViewFavoriteViewSet,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
|
||||
IssueViewViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
|
||||
IssueViewViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/",
|
||||
GlobalViewViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="global-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/<uuid:pk>/",
|
||||
GlobalViewViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="global-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/issues/",
|
||||
GlobalViewIssuesViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
name="global-view-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
|
||||
IssueViewFavoriteViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="user-favorite-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
|
||||
IssueViewFavoriteViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="user-favorite-view",
|
||||
),
|
||||
]
|
||||
@@ -1,182 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.api.views import (
|
||||
WorkSpaceViewSet,
|
||||
InviteWorkspaceEndpoint,
|
||||
WorkSpaceMemberViewSet,
|
||||
WorkspaceMembersEndpoint,
|
||||
WorkspaceInvitationsViewset,
|
||||
WorkspaceMemberUserEndpoint,
|
||||
WorkspaceMemberUserViewsEndpoint,
|
||||
WorkSpaceAvailabilityCheckEndpoint,
|
||||
TeamMemberViewSet,
|
||||
UserLastProjectWithWorkspaceEndpoint,
|
||||
WorkspaceThemeViewSet,
|
||||
WorkspaceUserProfileStatsEndpoint,
|
||||
WorkspaceUserActivityEndpoint,
|
||||
WorkspaceUserProfileEndpoint,
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
LeaveWorkspaceEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspace-slug-check/",
|
||||
WorkSpaceAvailabilityCheckEndpoint.as_view(),
|
||||
name="workspace-availability",
|
||||
),
|
||||
path(
|
||||
"workspaces/",
|
||||
WorkSpaceViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/",
|
||||
WorkSpaceViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/invite/",
|
||||
InviteWorkspaceEndpoint.as_view(),
|
||||
name="invite-workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/invitations/",
|
||||
WorkspaceInvitationsViewset.as_view({"get": "list"}),
|
||||
name="workspace-invitations",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/invitations/<uuid:pk>/",
|
||||
WorkspaceInvitationsViewset.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
}
|
||||
),
|
||||
name="workspace-invitations",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/",
|
||||
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||
name="workspace-member",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/<uuid:pk>/",
|
||||
WorkSpaceMemberViewSet.as_view(
|
||||
{
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
}
|
||||
),
|
||||
name="workspace-member",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-members/",
|
||||
WorkspaceMembersEndpoint.as_view(),
|
||||
name="workspace-members",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/teams/",
|
||||
TeamMemberViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="workspace-team-members",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/teams/<uuid:pk>/",
|
||||
TeamMemberViewSet.as_view(
|
||||
{
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
}
|
||||
),
|
||||
name="workspace-team-members",
|
||||
),
|
||||
path(
|
||||
"users/last-visited-workspace/",
|
||||
UserLastProjectWithWorkspaceEndpoint.as_view(),
|
||||
name="workspace-project-details",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-members/me/",
|
||||
WorkspaceMemberUserEndpoint.as_view(),
|
||||
name="workspace-member-details",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-views/",
|
||||
WorkspaceMemberUserViewsEndpoint.as_view(),
|
||||
name="workspace-member-views-details",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-themes/",
|
||||
WorkspaceThemeViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="workspace-themes",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
|
||||
WorkspaceThemeViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="workspace-themes",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-stats/<uuid:user_id>/",
|
||||
WorkspaceUserProfileStatsEndpoint.as_view(),
|
||||
name="workspace-user-stats",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-activity/<uuid:user_id>/",
|
||||
WorkspaceUserActivityEndpoint.as_view(),
|
||||
name="workspace-user-activity",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-profile/<uuid:user_id>/",
|
||||
WorkspaceUserProfileEndpoint.as_view(),
|
||||
name="workspace-user-profile-page",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-issues/<uuid:user_id>/",
|
||||
WorkspaceUserProfileIssuesEndpoint.as_view(),
|
||||
name="workspace-user-profile-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/labels/",
|
||||
WorkspaceLabelsEndpoint.as_view(),
|
||||
name="workspace-labels",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/leave/",
|
||||
LeaveWorkspaceEndpoint.as_view(),
|
||||
name="leave-workspace-members",
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
from .project import (
|
||||
ProjectViewSet,
|
||||
ProjectSettingViewSet,
|
||||
ProjectMemberViewSet,
|
||||
UserProjectInvitationsViewset,
|
||||
InviteProjectEndpoint,
|
||||
|
||||
@@ -9,7 +9,7 @@ from sentry_sdk import capture_exception
|
||||
# Module imports
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Project, Estimate, EstimatePoint
|
||||
from plane.db.models import ProjectSetting, Estimate, EstimatePoint
|
||||
from plane.api.serializers import (
|
||||
EstimateSerializer,
|
||||
EstimatePointSerializer,
|
||||
@@ -24,10 +24,10 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
try:
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
if project.estimate_id is not None:
|
||||
project_setting = ProjectSetting.objects.get(workspace__slug=slug, pk=project_id)
|
||||
if project_setting.estimate_id is not None:
|
||||
estimate_points = EstimatePoint.objects.filter(
|
||||
estimate_id=project.estimate_id,
|
||||
estimate_id=project_setting.estimate_id,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
@@ -63,6 +63,7 @@ from plane.api.permissions import (
|
||||
)
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectSetting,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
@@ -293,13 +294,14 @@ class IssueViewSet(BaseViewSet):
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
project = Project.objects.get(pk=project_id)
|
||||
project_setting = ProjectSetting.objects.get(workspace__slug=slug, project_id=project_id)
|
||||
|
||||
serializer = IssueCreateSerializer(
|
||||
data=request.data,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"workspace_id": project.workspace_id,
|
||||
"default_assignee_id": project.default_assignee_id,
|
||||
"default_assignee_id": project_setting.default_assignee_id,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2565,13 +2567,13 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
project_setting = ProjectSetting.objects.get(workspace__slug=slug, project_id=project_id)
|
||||
serializer = IssueCreateSerializer(
|
||||
data=request.data,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"workspace_id": project.workspace_id,
|
||||
"default_assignee_id": project.default_assignee_id,
|
||||
"default_assignee_id": project_setting.default_assignee_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -29,11 +29,12 @@ from sentry_sdk import capture_exception
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.api.serializers import (
|
||||
ProjectSerializer,
|
||||
ProjectSettingDetailSerializer,
|
||||
ProjectSettingSerializer,
|
||||
ProjectMemberSerializer,
|
||||
ProjectDetailSerializer,
|
||||
ProjectMemberInviteSerializer,
|
||||
ProjectFavoriteSerializer,
|
||||
IssueLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
)
|
||||
@@ -67,6 +68,7 @@ from plane.db.models import (
|
||||
ModuleMember,
|
||||
Inbox,
|
||||
ProjectDeployBoard,
|
||||
ProjectSetting,
|
||||
)
|
||||
|
||||
from plane.bgtasks.project_invitation_task import project_invitation
|
||||
@@ -98,7 +100,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
|
||||
.select_related(
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
"workspace", "workspace__owner",
|
||||
)
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.annotate(
|
||||
@@ -210,20 +212,29 @@ class ProjectViewSet(BaseViewSet):
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
project_lead = request.data.get("project_lead", None)
|
||||
|
||||
# Create Project Setting
|
||||
_ = ProjectSetting.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
project_lead_id=request.data.get("project_lead", None)
|
||||
)
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
project_member = ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"], member=request.user, role=20
|
||||
)
|
||||
|
||||
if serializer.data["project_lead"] is not None and str(
|
||||
serializer.data["project_lead"]
|
||||
if project_lead is not None and str(
|
||||
project_lead
|
||||
) != str(request.user.id):
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member_id=serializer.data["project_lead"],
|
||||
member_id=project_lead,
|
||||
role=20,
|
||||
)
|
||||
|
||||
|
||||
# Default states
|
||||
states = [
|
||||
{
|
||||
@@ -307,7 +318,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -973,7 +984,7 @@ class ProjectFavoritesViewSet(BaseViewSet):
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(user=self.request.user)
|
||||
.select_related(
|
||||
"project", "project__project_lead", "project__default_assignee"
|
||||
"project",
|
||||
)
|
||||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
@@ -1246,3 +1257,69 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response([], status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProjectSettingViewSet(BaseViewSet):
|
||||
model = ProjectSetting
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_serializer_class(self, *args, **kwargs):
|
||||
if self.action in ["create", "partial_update"]:
|
||||
return ProjectSettingSerializer
|
||||
return ProjectSettingDetailSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
try:
|
||||
project_setting = self.get_queryset().first()
|
||||
|
||||
if project_setting is not None:
|
||||
serializer = ProjectSettingDetailSerializer(project_setting)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
{"error": "Project setting does not exists"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id):
|
||||
try:
|
||||
project_setting = self.get_queryset().first()
|
||||
|
||||
# Check if it is None
|
||||
if project_setting is not None:
|
||||
serializer = ProjectSettingSerializer(
|
||||
project_setting, data=request.data, partial=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"error": "Project setting does not exists"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue, Project, State
|
||||
from plane.db.models import Issue, ProjectSetting, State
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ def archive_and_close_old_issues():
|
||||
def archive_old_issues():
|
||||
try:
|
||||
# Get all the projects whose archive_in is greater than 0
|
||||
projects = Project.objects.filter(archive_in__gt=0)
|
||||
project_settings = ProjectSetting.objects.filter(archive_in__gt=0)
|
||||
|
||||
for project in projects:
|
||||
project_id = project.id
|
||||
archive_in = project.archive_in
|
||||
for project_setting in project_settings:
|
||||
project_id = project_setting.project_id
|
||||
archive_in = project_setting.archive_in
|
||||
|
||||
# Get all the issues whose updated_at in less that the archive_in month
|
||||
issues = Issue.issue_objects.filter(
|
||||
@@ -75,7 +75,7 @@ def archive_old_issues():
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_at": str(archive_at)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
actor_id=str(project_setting.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
@@ -95,13 +95,13 @@ def archive_old_issues():
|
||||
def close_old_issues():
|
||||
try:
|
||||
# Get all the projects whose close_in is greater than 0
|
||||
projects = Project.objects.filter(close_in__gt=0).select_related(
|
||||
"default_state"
|
||||
project_settings = ProjectSetting.objects.filter(close_in__gt=0).select_related(
|
||||
"close_state"
|
||||
)
|
||||
|
||||
for project in projects:
|
||||
project_id = project.id
|
||||
close_in = project.close_in
|
||||
for project_setting in project_settings:
|
||||
project_id = project_setting.project_id
|
||||
close_in = project_setting.close_in
|
||||
|
||||
# Get all the issues whose updated_at in less that the close_in month
|
||||
issues = Issue.issue_objects.filter(
|
||||
@@ -130,10 +130,10 @@ def close_old_issues():
|
||||
|
||||
# Check if Issues
|
||||
if issues:
|
||||
if project.default_state is None:
|
||||
if project_setting.close_state is None:
|
||||
close_state = State.objects.filter(group="cancelled").first()
|
||||
else:
|
||||
close_state = project.default_state
|
||||
close_state = project_setting.close_state
|
||||
|
||||
issues_to_update = []
|
||||
for issue in issues:
|
||||
@@ -147,7 +147,7 @@ def close_old_issues():
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
actor_id=str(project_setting.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# Generated by Django 4.2.3 on 2023-10-11 13:34
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def create_project_settings(apps, schema_editor):
|
||||
Project = apps.get_model("db", "Project")
|
||||
ProjectSetting = apps.get_model("db", "ProjectSetting")
|
||||
|
||||
ProjectSetting.objects.bulk_create(
|
||||
[
|
||||
ProjectSetting(
|
||||
project=project,
|
||||
workspace_id=project.workspace_id,
|
||||
archive_in=project.archive_in,
|
||||
close_in=project.close_in,
|
||||
cycle_view=project.cycle_view,
|
||||
default_assignee=project.default_assignee,
|
||||
default_state=project.default_state,
|
||||
estimate=project.estimate,
|
||||
inbox_view=project.inbox_view,
|
||||
issue_views_view=project.issue_views_view,
|
||||
module_view=project.module_view,
|
||||
page_view=project.page_view,
|
||||
project_lead=project.project_lead,
|
||||
)
|
||||
for project in Project.objects.all()
|
||||
],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProjectSetting',
|
||||
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)),
|
||||
('module_view', models.BooleanField(default=True)),
|
||||
('cycle_view', models.BooleanField(default=True)),
|
||||
('issue_views_view', models.BooleanField(default=True)),
|
||||
('page_view', models.BooleanField(default=True)),
|
||||
('inbox_view', models.BooleanField(default=False)),
|
||||
('archive_in', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(12)])),
|
||||
('close_in', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(12)])),
|
||||
('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')),
|
||||
('default_assignee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='default_assignee', to=settings.AUTH_USER_MODEL)),
|
||||
('default_state', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_state', to='db.state')),
|
||||
('estimate', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='db.estimate')),
|
||||
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='project_settings', to='db.project')),
|
||||
('project_lead', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_lead', to=settings.AUTH_USER_MODEL)),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_settings', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Project Settings',
|
||||
'verbose_name_plural': 'Project Settings',
|
||||
'db_table': 'project_settings',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(create_project_settings),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='archive_in',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='close_in',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='cycle_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='default_assignee',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='default_state',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='description_html',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='description_text',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='estimate',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='inbox_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='issue_views_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='module_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='page_view',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='project_lead',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.3 on 2023-10-12 07:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0046_auto_20231011_1334'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='projectsetting',
|
||||
name='default_state',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projectsetting',
|
||||
name='close_state',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='close_state', to='db.state'),
|
||||
),
|
||||
]
|
||||
@@ -13,6 +13,7 @@ from .workspace import (
|
||||
|
||||
from .project import (
|
||||
Project,
|
||||
ProjectSetting,
|
||||
ProjectMember,
|
||||
ProjectBaseModel,
|
||||
ProjectMemberInvite,
|
||||
|
||||
@@ -38,7 +38,7 @@ def get_default_props():
|
||||
},
|
||||
"display_filters": {
|
||||
"group_by": None,
|
||||
"order_by": '-created_at',
|
||||
"order_by": "-created_at",
|
||||
"type": None,
|
||||
"sub_issue": True,
|
||||
"show_empty_groups": True,
|
||||
@@ -56,12 +56,6 @@ class Project(BaseModel):
|
||||
NETWORK_CHOICES = ((0, "Secret"), (2, "Public"))
|
||||
name = models.CharField(max_length=255, verbose_name="Project Name")
|
||||
description = models.TextField(verbose_name="Project Description", blank=True)
|
||||
description_text = models.JSONField(
|
||||
verbose_name="Project Description RT", blank=True, null=True
|
||||
)
|
||||
description_html = models.JSONField(
|
||||
verbose_name="Project Description HTML", blank=True, null=True
|
||||
)
|
||||
network = models.PositiveSmallIntegerField(default=2, choices=NETWORK_CHOICES)
|
||||
workspace = models.ForeignKey(
|
||||
"db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_project"
|
||||
@@ -70,40 +64,40 @@ class Project(BaseModel):
|
||||
max_length=12,
|
||||
verbose_name="Project Identifier",
|
||||
)
|
||||
default_assignee = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="default_assignee",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
project_lead = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="project_lead",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
# default_assignee = models.ForeignKey(
|
||||
# settings.AUTH_USER_MODEL,
|
||||
# on_delete=models.CASCADE,
|
||||
# related_name="default_assignee",
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# )
|
||||
# project_lead = models.ForeignKey(
|
||||
# settings.AUTH_USER_MODEL,
|
||||
# on_delete=models.CASCADE,
|
||||
# related_name="project_lead",
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# )
|
||||
emoji = models.CharField(max_length=255, null=True, blank=True)
|
||||
icon_prop = models.JSONField(null=True)
|
||||
module_view = models.BooleanField(default=True)
|
||||
cycle_view = models.BooleanField(default=True)
|
||||
issue_views_view = models.BooleanField(default=True)
|
||||
page_view = models.BooleanField(default=True)
|
||||
inbox_view = models.BooleanField(default=False)
|
||||
# module_view = models.BooleanField(default=True)
|
||||
# cycle_view = models.BooleanField(default=True)
|
||||
# issue_views_view = models.BooleanField(default=True)
|
||||
# page_view = models.BooleanField(default=True)
|
||||
# inbox_view = models.BooleanField(default=False)
|
||||
cover_image = models.URLField(blank=True, null=True, max_length=800)
|
||||
estimate = models.ForeignKey(
|
||||
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
|
||||
)
|
||||
archive_in = models.IntegerField(
|
||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
)
|
||||
close_in = models.IntegerField(
|
||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
)
|
||||
default_state = models.ForeignKey(
|
||||
"db.State", on_delete=models.SET_NULL, null=True, related_name="default_state"
|
||||
)
|
||||
# estimate = models.ForeignKey(
|
||||
# "db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
|
||||
# )
|
||||
# archive_in = models.IntegerField(
|
||||
# default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
# )
|
||||
# close_in = models.IntegerField(
|
||||
# default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
# )
|
||||
# default_state = models.ForeignKey(
|
||||
# "db.State", on_delete=models.SET_NULL, null=True, related_name="default_state"
|
||||
# )
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the project"""
|
||||
@@ -121,6 +115,60 @@ class Project(BaseModel):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ProjectSetting(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="project_settings"
|
||||
)
|
||||
project = models.OneToOneField(
|
||||
"db.Project", on_delete=models.CASCADE, related_name="project_settings"
|
||||
)
|
||||
default_assignee = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="default_assignee",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
project_lead = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="project_lead",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
module_view = models.BooleanField(default=True)
|
||||
cycle_view = models.BooleanField(default=True)
|
||||
issue_views_view = models.BooleanField(default=True)
|
||||
page_view = models.BooleanField(default=True)
|
||||
inbox_view = models.BooleanField(default=False)
|
||||
estimate = models.ForeignKey(
|
||||
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
|
||||
)
|
||||
archive_in = models.IntegerField(
|
||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
)
|
||||
close_in = models.IntegerField(
|
||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
)
|
||||
close_state = models.ForeignKey(
|
||||
"db.State", on_delete=models.SET_NULL, null=True, related_name="close_state"
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.workspace = self.project.workspace
|
||||
super(ProjectSetting, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the project"""
|
||||
return f"{self.project.name} <{self.workspace.name}>"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Project Settings"
|
||||
verbose_name_plural = "Project Settings"
|
||||
db_table = "project_settings"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class ProjectBaseModel(BaseModel):
|
||||
project = models.ForeignKey(
|
||||
Project, on_delete=models.CASCADE, related_name="project_%(class)s"
|
||||
|
||||
@@ -13,11 +13,10 @@ import { renderEmoji } from "helpers/emoji.helper";
|
||||
type Props = {
|
||||
projectId?: string | string[];
|
||||
commentId: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const CommentReaction: React.FC<Props> = (props) => {
|
||||
const { projectId, commentId, readonly = false } = props;
|
||||
const { projectId, commentId } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@@ -48,18 +47,16 @@ export const CommentReaction: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div className="flex gap-1.5 items-center mt-2">
|
||||
{!readonly && (
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={
|
||||
commentReactions
|
||||
?.filter((reaction) => reaction.actor === user?.id)
|
||||
.map((r) => r.reaction) || []
|
||||
}
|
||||
onSelect={handleReactionClick}
|
||||
/>
|
||||
)}
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={
|
||||
commentReactions
|
||||
?.filter((reaction) => reaction.actor === user?.id)
|
||||
.map((r) => r.reaction) || []
|
||||
}
|
||||
onSelect={handleReactionClick}
|
||||
/>
|
||||
|
||||
{Object.keys(groupedReactions || {}).map(
|
||||
(reaction) =>
|
||||
@@ -67,7 +64,6 @@ export const CommentReaction: React.FC<Props> = (props) => {
|
||||
groupedReactions[reaction].length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={readonly}
|
||||
onClick={() => {
|
||||
handleReactionClick(reaction);
|
||||
}}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const SidebarCycleSelect: React.FC<Props> = ({
|
||||
|
||||
issuesService
|
||||
.removeIssueFromCycle(workspaceSlug as string, projectId as string, cycleId, bridgeId)
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
|
||||
mutate(CYCLE_ISSUES(cycleId));
|
||||
|
||||
@@ -41,7 +41,9 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => (
|
||||
const UserLink = ({ activity }: { activity: IIssueActivity }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => console.log("user", activity.new_identifier ?? activity.old_identifier)}
|
||||
onClick={() => {
|
||||
console.log("user", activity.actor);
|
||||
}}
|
||||
className="font-medium text-custom-text-100 inline-flex items-center hover:underline"
|
||||
>
|
||||
{activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value}
|
||||
@@ -107,25 +109,9 @@ const activityDetails: {
|
||||
{activity.old_value === ""
|
||||
? "marked this issue is blocking issue "
|
||||
: "removed the blocking issue "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"issue",
|
||||
JSON.stringify({
|
||||
project_id: activity.project,
|
||||
issue_id: activity.new_identifier ?? activity.old_identifier,
|
||||
issue_identifier:
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? activity.new_value
|
||||
: activity.old_value,
|
||||
})
|
||||
)
|
||||
}
|
||||
className="font-medium text-custom-text-100"
|
||||
>
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.old_value === "" ? activity.new_value : activity.old_value}
|
||||
</button>
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
),
|
||||
@@ -138,25 +124,9 @@ const activityDetails: {
|
||||
{activity.old_value === ""
|
||||
? "marked this issue is being blocked by issue "
|
||||
: "removed this issue being blocked by issue "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"issue",
|
||||
JSON.stringify({
|
||||
project_id: activity.project,
|
||||
issue_id: activity.new_identifier ?? activity.old_identifier,
|
||||
issue_identifier:
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? activity.new_value
|
||||
: activity.old_value,
|
||||
})
|
||||
)
|
||||
}
|
||||
className="font-medium text-custom-text-100"
|
||||
>
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.old_value === "" ? activity.new_value : activity.old_value}
|
||||
</button>
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
),
|
||||
@@ -169,25 +139,9 @@ const activityDetails: {
|
||||
{activity.old_value === ""
|
||||
? "marked this issue as duplicate of "
|
||||
: "removed this issue as a duplicate of "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"issue",
|
||||
JSON.stringify({
|
||||
project_id: activity.project,
|
||||
issue_id: activity.new_identifier ?? activity.old_identifier,
|
||||
issue_identifier:
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? activity.new_value
|
||||
: activity.old_value,
|
||||
})
|
||||
)
|
||||
}
|
||||
className="font-medium text-custom-text-100"
|
||||
>
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.verb === "created" ? activity.new_value : activity.old_value}
|
||||
</button>
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
),
|
||||
@@ -200,25 +154,9 @@ const activityDetails: {
|
||||
{activity.old_value === ""
|
||||
? "marked that this issue relates to "
|
||||
: "removed the relation from "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
console.log(
|
||||
"issue",
|
||||
JSON.stringify({
|
||||
project_id: activity.project,
|
||||
issue_id: activity.new_identifier ?? activity.old_identifier,
|
||||
issue_identifier:
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? activity.new_value
|
||||
: activity.old_value,
|
||||
})
|
||||
);
|
||||
}}
|
||||
className="font-medium text-custom-text-100"
|
||||
>
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.old_value === "" ? activity.new_value : activity.old_value}
|
||||
</button>
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
),
|
||||
@@ -237,17 +175,14 @@ const activityDetails: {
|
||||
console.log(
|
||||
"cycle",
|
||||
JSON.stringify({
|
||||
cycle_id: activity.new_identifier ?? activity.old_identifier,
|
||||
cycle_id: activity.new_identifier,
|
||||
project_id: activity.project,
|
||||
cycle_name: activity.verb === "created" ? activity.new_value : activity.old_value,
|
||||
})
|
||||
)
|
||||
}
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.verb === "created" || activity.verb === "updated"
|
||||
? activity.new_value
|
||||
: activity.old_value}
|
||||
{activity.new_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</button>
|
||||
</>
|
||||
@@ -360,23 +295,16 @@ const activityDetails: {
|
||||
{activity.verb === "created" && "added this "}
|
||||
{activity.verb === "updated" && "updated this "}
|
||||
{activity.verb === "deleted" && "removed this "}
|
||||
module{" "}
|
||||
<button
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"module",
|
||||
JSON.stringify({
|
||||
module_id: activity.new_identifier ?? activity.old_identifier,
|
||||
project_id: activity.project,
|
||||
module_name: activity.verb === "created" ? activity.new_value : activity.old_value,
|
||||
})
|
||||
activity.verb === "created" ? activity.new_value : activity.old_value
|
||||
)
|
||||
}
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.verb === "created" || activity.verb === "updated"
|
||||
? activity.new_value
|
||||
: activity.old_value}
|
||||
module
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</button>
|
||||
.
|
||||
@@ -405,25 +333,9 @@ const activityDetails: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
{activity.new_value ? "set the parent to " : "removed the parent "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
console.log(
|
||||
"issue",
|
||||
JSON.stringify({
|
||||
project_id: activity.project,
|
||||
issue_id: activity.new_identifier ?? activity.old_identifier,
|
||||
issue_identifier:
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? activity.new_value
|
||||
: activity.old_value,
|
||||
})
|
||||
);
|
||||
}}
|
||||
className="font-medium text-custom-text-100"
|
||||
>
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.new_value ? activity.new_value : activity.old_value}
|
||||
</button>
|
||||
</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// icons
|
||||
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// ui
|
||||
import { CustomMenu, Icon } from "components/ui";
|
||||
import { CommentReaction } from "components/issues";
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { IIssueComment } from "types";
|
||||
|
||||
type Props = {
|
||||
comment: IIssueComment;
|
||||
handleCommentDeletion: (comment: string) => void;
|
||||
onSubmit: (commentId: string, data: Partial<IIssueComment>) => void;
|
||||
showAccessSpecifier?: boolean;
|
||||
workspaceSlug: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const CommentCard: React.FC<Props> = (props) => {
|
||||
const {
|
||||
comment,
|
||||
handleCommentDeletion,
|
||||
onSubmit,
|
||||
showAccessSpecifier = false,
|
||||
workspaceSlug,
|
||||
disabled,
|
||||
} = props;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const editorRef = React.useRef<any>(null);
|
||||
const showEditorRef = React.useRef<any>(null);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<IIssueComment>({
|
||||
defaultValues: comment,
|
||||
});
|
||||
|
||||
const onEnter = (formData: Partial<IIssueComment>) => {
|
||||
if (isSubmitting) return;
|
||||
setIsEditing(false);
|
||||
|
||||
onSubmit(comment.id, formData);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isEditing && setFocus("comment");
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={
|
||||
comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name + " Bot"
|
||||
: comment.actor_detail.display_name
|
||||
}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
|
||||
>
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name.charAt(0)
|
||||
: comment.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||
<ChatBubbleLeftEllipsisIcon
|
||||
className="h-3.5 w-3.5 text-custom-text-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name + " Bot"
|
||||
: comment.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
commented {timeAgo(comment.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<form
|
||||
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
|
||||
onSubmit={handleSubmit(onEnter)}
|
||||
>
|
||||
<div>
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={watch("comment_html")}
|
||||
debouncedUpdatesEnabled={false}
|
||||
customClassName="min-h-[50px] p-3 shadow-sm"
|
||||
onChange={(comment_json: Object, comment_html: string) => {
|
||||
setValue("comment_json", comment_json);
|
||||
setValue("comment_html", comment_html);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || disabled}
|
||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
>
|
||||
<CheckIcon className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className={`relative ${isEditing ? "hidden" : ""}`}>
|
||||
{showAccessSpecifier && (
|
||||
<div className="absolute top-1 right-1.5 z-[1] text-custom-text-300">
|
||||
<Icon
|
||||
iconName={comment.access === "INTERNAL" ? "lock" : "public"}
|
||||
className="!text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html}
|
||||
editable={false}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
/>
|
||||
<CommentReaction
|
||||
readonly={disabled}
|
||||
projectId={comment.project}
|
||||
commentId={comment.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user?.id === comment.actor && !disabled && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Icon iconName="edit" />
|
||||
Edit comment
|
||||
</CustomMenu.MenuItem>
|
||||
{showAccessSpecifier && (
|
||||
<>
|
||||
{comment.access === "INTERNAL" ? (
|
||||
<CustomMenu.MenuItem
|
||||
renderAs="button"
|
||||
onClick={() => onSubmit(comment.id, { access: "EXTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Icon iconName="public" />
|
||||
Switch to public comment
|
||||
</CustomMenu.MenuItem>
|
||||
) : (
|
||||
<CustomMenu.MenuItem
|
||||
renderAs="button"
|
||||
onClick={() => onSubmit(comment.id, { access: "INTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Icon iconName="lock" />
|
||||
Switch to private comment
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCommentDeletion(comment.id);
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Icon iconName="delete" />
|
||||
Delete comment
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { WebViewModal } from "components/web-view";
|
||||
|
||||
type DeleteConfirmationProps = {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
content: string | React.ReactNode;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
export const DeleteConfirmation: React.FC<DeleteConfirmationProps> = (props) => {
|
||||
const { isOpen, onCancel, onConfirm, title, content } = props;
|
||||
|
||||
return (
|
||||
<WebViewModal isOpen={isOpen} onClose={onCancel} modalTitle={title}>
|
||||
<div className="text-custom-text-200">
|
||||
<p>{content}</p>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
className="w-full py-2 flex items-center justify-center rounded-[4px] bg-red-500/10 text-red-500 border border-red-500 text-base font-medium"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</WebViewModal>
|
||||
);
|
||||
};
|
||||
@@ -1,188 +0,0 @@
|
||||
// react
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// react date-picker
|
||||
import DatePicker, { ReactDatePickerProps } from "react-datepicker";
|
||||
|
||||
// components
|
||||
import { WebViewModal } from "./web-view-modal";
|
||||
import { SecondaryButton, PrimaryButton } from "components/ui";
|
||||
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
interface Props extends ReactDatePickerProps {
|
||||
value: string | undefined;
|
||||
onChange: (value: any) => void;
|
||||
disabled?: boolean;
|
||||
renderAs?: "input" | "button";
|
||||
error?: any;
|
||||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
export const DateSelector: React.FC<Props> = (props) => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
renderAs = "button",
|
||||
noBorder = true,
|
||||
error,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) setSelectedDate(new Date(value));
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
if (value) setSelectedDate(new Date(value));
|
||||
else setSelectedDate(new Date());
|
||||
}, [isOpen, value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebViewModal
|
||||
isOpen={isOpen}
|
||||
modalTitle="Select due-date"
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<DatePicker
|
||||
inline
|
||||
selected={selectedDate ? new Date(selectedDate) : null}
|
||||
className={`${
|
||||
renderAs === "input"
|
||||
? "block px-2 py-2 text-sm focus:outline-none"
|
||||
: renderAs === "button"
|
||||
? `px-2 py-1 text-xs shadow-sm ${
|
||||
disabled ? "" : "hover:bg-custom-background-80"
|
||||
} duration-300`
|
||||
: ""
|
||||
} ${error ? "border-red-500 bg-red-100" : ""} ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} ${
|
||||
noBorder ? "" : "border border-custom-border-200"
|
||||
} w-full rounded-md caret-transparent outline-none ${className}`}
|
||||
dateFormat="MMM dd, yyyy"
|
||||
{...props}
|
||||
onChange={(val) => {
|
||||
if (!val) setSelectedDate(null);
|
||||
else setSelectedDate(val);
|
||||
}}
|
||||
renderCustomHeader={({
|
||||
date,
|
||||
decreaseMonth,
|
||||
increaseMonth,
|
||||
prevMonthButtonDisabled,
|
||||
nextMonthButtonDisabled,
|
||||
}) => (
|
||||
<div className="flex justify-between px-5 text-lg font-medium">
|
||||
<h4>
|
||||
{date.toLocaleString("default", { month: "long" })} {date.getFullYear()}
|
||||
</h4>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={decreaseMonth}
|
||||
disabled={prevMonthButtonDisabled}
|
||||
className="text-custom-text-100"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.2285 14.5416L7.10352 10.4166C7.03407 10.3472 6.98546 10.2778 6.95768 10.2083C6.9299 10.1389 6.91602 10.0625 6.91602 9.97915C6.91602 9.89581 6.9299 9.81942 6.95768 9.74998C6.98546 9.68053 7.03407 9.61109 7.10352 9.54165L11.2493 5.39581C11.3743 5.27081 11.5237 5.20831 11.6973 5.20831C11.8709 5.20831 12.0202 5.27081 12.1452 5.39581C12.2702 5.52081 12.3292 5.67359 12.3223 5.85415C12.3153 6.0347 12.2493 6.18748 12.1243 6.31248L8.45768 9.97915L12.1452 13.6666C12.2702 13.7916 12.3327 13.9375 12.3327 14.1041C12.3327 14.2708 12.2702 14.4166 12.1452 14.5416C12.0202 14.6666 11.8674 14.7291 11.6868 14.7291C11.5063 14.7291 11.3535 14.6666 11.2285 14.5416Z"
|
||||
fill="#171717"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={increaseMonth}
|
||||
disabled={nextMonthButtonDisabled}
|
||||
className="text-custom-text-100"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.37496 14.5417C7.26385 14.4028 7.20482 14.25 7.19788 14.0834C7.19093 13.9167 7.24996 13.7709 7.37496 13.6459L11.0416 9.97919L7.35413 6.29169C7.24302 6.18058 7.19093 6.03128 7.19788 5.84378C7.20482 5.65628 7.26385 5.50697 7.37496 5.39586C7.51385 5.25697 7.66316 5.191 7.82288 5.19794C7.9826 5.20489 8.12496 5.27086 8.24996 5.39586L12.3958 9.54169C12.4652 9.61114 12.5139 9.68058 12.5416 9.75003C12.5694 9.81947 12.5833 9.89586 12.5833 9.97919C12.5833 10.0625 12.5694 10.1389 12.5416 10.2084C12.5139 10.2778 12.4652 10.3473 12.3958 10.4167L8.27079 14.5417C8.14579 14.6667 7.99996 14.7257 7.83329 14.7188C7.66663 14.7118 7.51385 14.6528 7.37496 14.5417Z"
|
||||
fill="#171717"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<WebViewModal.Footer className="flex items-center gap-2">
|
||||
<SecondaryButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
onChange(null);
|
||||
setSelectedDate(null);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
Clear
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (!selectedDate) onChange(null);
|
||||
else onChange(renderDateFormat(selectedDate));
|
||||
setIsOpen(false);
|
||||
}}
|
||||
type="button"
|
||||
className="w-full"
|
||||
>
|
||||
Apply
|
||||
</PrimaryButton>
|
||||
</WebViewModal.Footer>
|
||||
</WebViewModal>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-200"
|
||||
}
|
||||
>
|
||||
{value ? (
|
||||
<div className="-my-0.5 flex items-center gap-2">
|
||||
{new Date(value).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
"Due date"
|
||||
)}
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -13,15 +13,6 @@ export * from "./select-assignee";
|
||||
export * from "./select-estimate";
|
||||
export * from "./add-comment";
|
||||
export * from "./select-parent";
|
||||
export * from "./select-blocker-to";
|
||||
export * from "./select-blocked-by";
|
||||
export * from "./select-blocker";
|
||||
export * from "./select-blocked";
|
||||
export * from "./activity-message";
|
||||
export * from "./issues-select-bottom-sheet";
|
||||
export * from "./select-relates-to";
|
||||
export * from "./select-duplicate";
|
||||
export * from "./spinner";
|
||||
export * from "./select-module";
|
||||
export * from "./select-cycle";
|
||||
export * from "./confirm-delete";
|
||||
export * from "./commend-card";
|
||||
export * from "./date-selector";
|
||||
|
||||
@@ -17,7 +17,8 @@ import issuesService from "services/issues.service";
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
// components
|
||||
import { Label, AddComment, ActivityMessage, ActivityIcon, CommentCard } from "components/web-view";
|
||||
import { CommentCard } from "components/issues/comment";
|
||||
import { Label, AddComment, ActivityMessage, ActivityIcon } from "components/web-view";
|
||||
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
@@ -53,32 +54,23 @@ export const IssueActivity: React.FC<Props> = (props) => {
|
||||
: null
|
||||
);
|
||||
|
||||
const handleCommentUpdate = async (comment: any, formData: any) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || !allowed) return;
|
||||
const handleCommentUpdate = async (comment: any) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
await issuesService
|
||||
.patchIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
comment.id,
|
||||
comment,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.then(() => mutateIssueActivity())
|
||||
.catch(() =>
|
||||
console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: "Comment could not be updated. Please try again.",
|
||||
})
|
||||
)
|
||||
);
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
const handleCommentDelete = async (commentId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || !allowed) return;
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||
|
||||
@@ -90,20 +82,11 @@ export const IssueActivity: React.FC<Props> = (props) => {
|
||||
commentId,
|
||||
user
|
||||
)
|
||||
.then(() => mutateIssueActivity())
|
||||
.catch(() =>
|
||||
console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: "Comment could not be deleted. Please try again.",
|
||||
})
|
||||
)
|
||||
);
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
const handleAddComment = async (formData: IIssueComment) => {
|
||||
if (!workspaceSlug || !issueDetails || !allowed) return;
|
||||
if (!workspaceSlug || !issueDetails) return;
|
||||
|
||||
await issuesService
|
||||
.createIssueComment(
|
||||
@@ -121,6 +104,7 @@ export const IssueActivity: React.FC<Props> = (props) => {
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Comment could not be posted. Please try again.",
|
||||
})
|
||||
)
|
||||
@@ -130,7 +114,7 @@ export const IssueActivity: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<Label>Activity</Label>
|
||||
<div className="mt-1 space-y-[6px] p-2 border border-custom-border-200 rounded-[4px]">
|
||||
<div className="mt-1 space-y-[6px] p-2 border rounded-[4px]">
|
||||
<ul role="list" className="-mb-4">
|
||||
{issueActivities?.map((activityItem, index) => {
|
||||
// determines what type of action is performed
|
||||
@@ -224,31 +208,23 @@ export const IssueActivity: React.FC<Props> = (props) => {
|
||||
comment={activityItem as any}
|
||||
onSubmit={handleCommentUpdate}
|
||||
handleCommentDeletion={handleCommentDelete}
|
||||
disabled={
|
||||
!allowed ||
|
||||
!issueDetails ||
|
||||
issueDetails.state === "closed" ||
|
||||
issueDetails.state === "archived"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{allowed && (
|
||||
<li>
|
||||
<div className="my-4">
|
||||
<AddComment
|
||||
onSubmit={handleAddComment}
|
||||
disabled={
|
||||
!allowed ||
|
||||
!issueDetails ||
|
||||
issueDetails.state === "closed" ||
|
||||
issueDetails.state === "archived"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<div className="my-4">
|
||||
<AddComment
|
||||
onSubmit={handleAddComment}
|
||||
disabled={
|
||||
!allowed ||
|
||||
!issueDetails ||
|
||||
issueDetails.state === "closed" ||
|
||||
issueDetails.state === "archived"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,10 +21,8 @@ import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys
|
||||
import { FileText, ChevronRight, X, Image as ImageIcon } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { Label, WebViewModal, DeleteConfirmation } from "components/web-view";
|
||||
|
||||
// helpers
|
||||
import { getFileName } from "helpers/attachment.helper";
|
||||
import { Label, WebViewModal } from "components/web-view";
|
||||
import { DeleteAttachmentModal } from "components/issues";
|
||||
|
||||
// types
|
||||
import type { IIssueAttachment } from "types";
|
||||
@@ -49,7 +47,7 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
if (!acceptedFiles[0] || !workspaceSlug || !allowed) return;
|
||||
if (!acceptedFiles[0] || !workspaceSlug) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("asset", acceptedFiles[0]);
|
||||
@@ -99,37 +97,9 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
);
|
||||
});
|
||||
},
|
||||
[issueId, projectId, workspaceSlug, allowed]
|
||||
[issueId, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleDeletion = async (assetId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
mutate<IIssueAttachment[]>(
|
||||
ISSUE_ATTACHMENTS(issueId as string),
|
||||
(prevData) => (prevData ?? [])?.filter((p) => p.id !== assetId),
|
||||
false
|
||||
);
|
||||
|
||||
await issuesService
|
||||
.deleteIssueAttachment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
assetId as string
|
||||
)
|
||||
.then(() => mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)))
|
||||
.catch(() => {
|
||||
console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: "Something went wrong please try again.",
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop,
|
||||
maxSize: 5 * 1024 * 1024,
|
||||
@@ -150,24 +120,10 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DeleteConfirmation
|
||||
title="Delete Attachment"
|
||||
content={
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete attachment-{" "}
|
||||
<span className="font-bold">
|
||||
{getFileName(deleteAttachment?.attributes?.name ?? "")}
|
||||
</span>
|
||||
? This attachment will be permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
}
|
||||
<DeleteAttachmentModal
|
||||
isOpen={allowed && attachmentDeleteModal}
|
||||
onCancel={() => setAttachmentDeleteModal(false)}
|
||||
onConfirm={() => {
|
||||
if (!deleteAttachment) return;
|
||||
handleDeletion(deleteAttachment.id);
|
||||
setAttachmentDeleteModal(false);
|
||||
}}
|
||||
setIsOpen={setAttachmentDeleteModal}
|
||||
data={deleteAttachment}
|
||||
/>
|
||||
|
||||
<WebViewModal isOpen={isOpen} onClose={() => setIsOpen(false)} modalTitle="Insert file">
|
||||
@@ -223,7 +179,6 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!allowed}
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="bg-custom-primary-100/10 border border-dotted rounded-[4px] border-custom-primary-100 text-center py-2 w-full text-custom-primary-100"
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ import issuesService from "services/issues.service";
|
||||
import { Link as LinkIcon, Plus, Pencil, X } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { Label, WebViewModal, CreateUpdateLinkForm, DeleteConfirmation } from "components/web-view";
|
||||
import { Label, WebViewModal, CreateUpdateLinkForm } from "components/web-view";
|
||||
|
||||
// ui
|
||||
import { SecondaryButton } from "components/ui";
|
||||
@@ -39,13 +39,12 @@ export const IssueLinks: React.FC<Props> = (props) => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedLink, setSelectedLink] = useState<string | null>(null);
|
||||
const [deleteSelected, setDeleteSelected] = useState<string | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const handleDeleteLink = async (linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetails || !allowed) return;
|
||||
if (!workspaceSlug || !projectId || !issueDetails) return;
|
||||
|
||||
const updatedLinks = issueDetails.issue_link.filter((l) => l.id !== linkId);
|
||||
|
||||
@@ -57,7 +56,7 @@ export const IssueLinks: React.FC<Props> = (props) => {
|
||||
|
||||
await issuesService
|
||||
.deleteIssueLink(workspaceSlug as string, projectId as string, issueDetails.id, linkId)
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
mutate(ISSUE_DETAILS(issueDetails.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -86,18 +85,6 @@ export const IssueLinks: React.FC<Props> = (props) => {
|
||||
/>
|
||||
</WebViewModal>
|
||||
|
||||
<DeleteConfirmation
|
||||
title="Delete Link"
|
||||
content="Are you sure you want to delete this link?"
|
||||
isOpen={!!deleteSelected}
|
||||
onCancel={() => setDeleteSelected(null)}
|
||||
onConfirm={() => {
|
||||
if (!deleteSelected || !allowed) return;
|
||||
handleDeleteLink(deleteSelected);
|
||||
setDeleteSelected(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Label>Links</Label>
|
||||
<div className="mt-1 space-y-[6px]">
|
||||
{links?.map((link) => (
|
||||
@@ -105,18 +92,14 @@ export const IssueLinks: React.FC<Props> = (props) => {
|
||||
key={link.id}
|
||||
className="px-3 border border-custom-border-200 rounded-[4px] py-2 flex justify-between items-center bg-custom-background-100"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
console.log("link", link.url);
|
||||
}}
|
||||
className="text-custom-text-200 truncate"
|
||||
>
|
||||
<span>
|
||||
<LinkIcon className="w-4 h-4 inline-block mr-1" />
|
||||
</span>
|
||||
<span>{link.title || link.metadata?.title || link.url}</span>
|
||||
</button>
|
||||
<Link href={link.url}>
|
||||
<a target="_blank" className="text-custom-text-200 truncate">
|
||||
<span>
|
||||
<LinkIcon className="w-4 h-4 inline-block mr-1" />
|
||||
</span>
|
||||
<span>{link.title}</span>
|
||||
</a>
|
||||
</Link>
|
||||
{allowed && (
|
||||
<div className="flex gap-2 items-center">
|
||||
<button
|
||||
@@ -131,7 +114,7 @@ export const IssueLinks: React.FC<Props> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDeleteSelected(link.id);
|
||||
handleDeleteLink(link.id);
|
||||
}}
|
||||
>
|
||||
<X className="w-[18px] h-[18px] text-custom-text-400" />
|
||||
|
||||
@@ -8,35 +8,26 @@ import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react hook forms
|
||||
import { Controller, useFormContext } 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";
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
// icons
|
||||
import {
|
||||
ChevronDown,
|
||||
PlayIcon,
|
||||
User,
|
||||
X,
|
||||
CalendarDays,
|
||||
LayoutGrid,
|
||||
Users,
|
||||
CopyPlus,
|
||||
} from "lucide-react";
|
||||
import { RectangleGroupIcon } from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon, BlockerIcon, RelatedIcon, ContrastIcon } from "components/icons";
|
||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
||||
import { ChevronDown, PlayIcon, User, X, CalendarDays, LayoutGrid, Users } from "lucide-react";
|
||||
|
||||
// hooks
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
|
||||
// ui
|
||||
import { SecondaryButton } from "components/ui";
|
||||
import { SecondaryButton, CustomDatePicker } from "components/ui";
|
||||
|
||||
// components
|
||||
import {
|
||||
@@ -47,72 +38,63 @@ import {
|
||||
EstimateSelect,
|
||||
ParentSelect,
|
||||
BlockerSelect,
|
||||
BlockedBySelect,
|
||||
RelatesSelect,
|
||||
DuplicateSelect,
|
||||
ModuleSelect,
|
||||
CycleSelect,
|
||||
DateSelector,
|
||||
BlockedSelect,
|
||||
} from "components/web-view";
|
||||
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
submitChanges: (data: Partial<IIssue>) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
const { submitChanges } = props;
|
||||
const { control, submitChanges } = props;
|
||||
|
||||
const { watch, control } = useFormContext<IIssue>();
|
||||
const blockerIssue =
|
||||
useWatch({
|
||||
control,
|
||||
name: "issue_relations",
|
||||
})?.filter((i) => i.relation_type === "blocked_by") || [];
|
||||
|
||||
const blockerIssues =
|
||||
watch("issue_relations")?.filter((i) => i.relation_type === "blocked_by") || [];
|
||||
const blockedIssue = useWatch({
|
||||
control,
|
||||
name: "related_issues",
|
||||
})?.filter((i) => i.relation_type === "blocked_by");
|
||||
|
||||
const blockedByIssues = watch("related_issues")?.filter((i) => i.relation_type === "blocked_by");
|
||||
|
||||
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,
|
||||
issue_detail: i.issue_detail,
|
||||
related_issue: i.issue_detail?.id,
|
||||
})),
|
||||
];
|
||||
|
||||
const duplicateIssuesRelation = [
|
||||
...(watch("related_issues")?.filter((i) => i.relation_type === "duplicate") ?? []),
|
||||
...(watch("issue_relations") ?? [])
|
||||
?.filter((i) => i.relation_type === "duplicate")
|
||||
.map((i) => ({
|
||||
...i,
|
||||
issue_detail: i.issue_detail,
|
||||
related_issue: i.issue_detail?.id,
|
||||
})),
|
||||
];
|
||||
|
||||
const startDate = watch("start_date");
|
||||
const startDate = useWatch({
|
||||
control,
|
||||
name: "start_date",
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const isArchive = Boolean(router.query.archive);
|
||||
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const [isViewAllOpen, setIsViewAllOpen] = useState(false);
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
|
||||
const handleMutation = (data: any) => {
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
return {
|
||||
...prevData,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-[6px]">
|
||||
<div>
|
||||
<Label>Details</Label>
|
||||
<div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<LayoutGrid className="h-4 w-4 flex-shrink-0 text-custom-text-400" />
|
||||
@@ -125,7 +107,6 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
render={({ field: { value } }) => (
|
||||
<StateSelect
|
||||
value={value}
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
onChange={(val: string) => submitChanges({ state: val })}
|
||||
/>
|
||||
)}
|
||||
@@ -133,9 +114,9 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1 text-custom-text-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
@@ -145,7 +126,7 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
>
|
||||
<path
|
||||
d="M13.5862 14.5239C13.3459 14.5239 13.1416 14.4398 12.9733 14.2715C12.805 14.1032 12.7209 13.8989 12.7209 13.6585V3.76429C12.7209 3.52391 12.805 3.31958 12.9733 3.15132C13.1416 2.98306 13.3459 2.89893 13.5862 2.89893C13.8266 2.89893 14.031 2.98306 14.1992 3.15132C14.3675 3.31958 14.4516 3.52391 14.4516 3.76429V13.6585C14.4516 13.8989 14.3675 14.1032 14.1992 14.2715C14.031 14.4398 13.8266 14.5239 13.5862 14.5239ZM5.1629 14.5239C5.04676 14.5239 4.93557 14.5018 4.82932 14.4576C4.72308 14.4133 4.63006 14.3513 4.55025 14.2715C4.47045 14.1917 4.40843 14.0986 4.36419 13.9922C4.31996 13.8858 4.29785 13.7746 4.29785 13.6585V11.2643C4.29785 11.0239 4.38198 10.8196 4.55025 10.6513C4.71851 10.4831 4.92283 10.3989 5.16322 10.3989C5.40359 10.3989 5.60791 10.4831 5.77618 10.6513C5.94445 10.8196 6.02859 11.0239 6.02859 11.2643V13.6585C6.02859 13.7746 6.00647 13.8858 5.96223 13.9922C5.91801 14.0986 5.85599 14.1917 5.77618 14.2715C5.69638 14.3513 5.60325 14.4133 5.49678 14.4576C5.39033 14.5018 5.27904 14.5239 5.1629 14.5239ZM9.37473 14.5239C9.13436 14.5239 8.93003 14.4398 8.76176 14.2715C8.59349 14.1032 8.50936 13.8989 8.50936 13.6585V7.5143C8.50936 7.27391 8.59349 7.06958 8.76176 6.90132C8.93003 6.73306 9.13436 6.64893 9.37473 6.64893C9.61511 6.64893 9.81943 6.73306 9.98771 6.90132C10.156 7.06958 10.2401 7.27391 10.2401 7.5143V13.6585C10.2401 13.8989 10.156 14.1032 9.98771 14.2715C9.81943 14.4398 9.61511 14.5239 9.37473 14.5239Z"
|
||||
fill="currentColor"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -158,7 +139,6 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
render={({ field: { value } }) => (
|
||||
<PrioritySelect
|
||||
value={value}
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
onChange={(val) => submitChanges({ priority: val })}
|
||||
/>
|
||||
)}
|
||||
@@ -166,7 +146,7 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<Users className="h-4 w-4 flex-shrink-0 text-custom-text-400" />
|
||||
@@ -179,14 +159,7 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
render={({ field: { value } }) => (
|
||||
<AssigneeSelect
|
||||
value={value}
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
onChange={(val: string) => {
|
||||
const assignees = value?.includes(val)
|
||||
? value?.filter((i) => i !== val)
|
||||
: [...(value ?? []), val];
|
||||
|
||||
submitChanges({ assignees_list: assignees });
|
||||
}}
|
||||
onChange={(val: string) => submitChanges({ assignees_list: [val] })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -196,7 +169,7 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
{isViewAllOpen && (
|
||||
<>
|
||||
{isEstimateActive && (
|
||||
<div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90 text-custom-text-400" />
|
||||
@@ -209,7 +182,6 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
render={({ field: { value } }) => (
|
||||
<EstimateSelect
|
||||
value={value}
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
onChange={(val) => submitChanges({ estimate_point: val })}
|
||||
/>
|
||||
)}
|
||||
@@ -218,7 +190,7 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="h-4 w-4 flex-shrink-0 text-custom-text-400" />
|
||||
@@ -231,7 +203,6 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
render={({ field: { value } }) => (
|
||||
<ParentSelect
|
||||
value={value}
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
onChange={(val) => submitChanges({ parent: val })}
|
||||
/>
|
||||
)}
|
||||
@@ -239,9 +210,7 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* blocker to / blocking */}
|
||||
<div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -250,62 +219,89 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<div>
|
||||
<BlockerSelect
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
value={null}
|
||||
onChange={(val) => {
|
||||
if (!user || !workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
issuesService
|
||||
.createIssueRelation(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
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>
|
||||
{blockerIssues &&
|
||||
blockerIssues.map((relation) => (
|
||||
{blockerIssue &&
|
||||
blockerIssue.map((issue) => (
|
||||
<div
|
||||
key={relation.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"
|
||||
>
|
||||
<span
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"issue",
|
||||
JSON.stringify({
|
||||
issue_id: relation.issue_detail?.id,
|
||||
project_id: relation.issue_detail?.project_detail.id,
|
||||
})
|
||||
)
|
||||
}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${issue.issue_detail?.project_detail.id}/issues/${issue.issue_detail?.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<BlockerIcon height={10} width={10} />
|
||||
{`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`}
|
||||
</span>
|
||||
{!isArchive && (
|
||||
<button
|
||||
type="button"
|
||||
className="duration-300"
|
||||
onClick={() => {
|
||||
if (memberRole.isGuest || memberRole.isViewer || !user) return;
|
||||
{`${issue.issue_detail?.project_detail.identifier}-${issue.issue_detail?.sequence_id}`}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="duration-300"
|
||||
onClick={() => {
|
||||
const updatedBlockers = blockerIssue.filter(
|
||||
(i) => i.issue_detail?.id !== issue.issue_detail?.id
|
||||
);
|
||||
|
||||
issuesService
|
||||
.deleteIssueRelation(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
relation.issue,
|
||||
relation.id,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-2 w-2" />
|
||||
</button>
|
||||
)}
|
||||
if (!user) return;
|
||||
|
||||
issuesService.deleteIssueRelation(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
issue.id,
|
||||
user
|
||||
);
|
||||
|
||||
handleMutation({
|
||||
issue_relations: updatedBlockers,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-2 w-2" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* blocked by */}
|
||||
<div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -313,190 +309,89 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
<span className="text-sm text-custom-text-400">Blocked by</span>
|
||||
</div>
|
||||
<div>
|
||||
<BlockedBySelect
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
<BlockedSelect
|
||||
value={null}
|
||||
onChange={(val) => {
|
||||
if (!user || !workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
issuesService
|
||||
.createIssueRelation(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
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>
|
||||
{blockedByIssues &&
|
||||
blockedByIssues.map((relation) => (
|
||||
{blockedIssue &&
|
||||
blockedIssue.map((issue) => (
|
||||
<div
|
||||
key={relation.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-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
||||
>
|
||||
<span
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"issue",
|
||||
JSON.stringify({
|
||||
issue_id: relation.issue_detail?.id,
|
||||
project_id: relation.issue_detail?.project_detail.id,
|
||||
})
|
||||
)
|
||||
}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${issue.issue_detail?.project_detail.id}/issues/${issue.issue_detail?.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<BlockedIcon height={10} width={10} />
|
||||
{`${relation?.issue_detail?.project_detail?.identifier}-${relation?.issue_detail?.sequence_id}`}
|
||||
</span>
|
||||
{!isArchive && !(memberRole.isGuest || memberRole.isViewer) && (
|
||||
<button
|
||||
type="button"
|
||||
className="duration-300"
|
||||
onClick={() => {
|
||||
if (memberRole.isGuest || memberRole.isViewer || !user) return;
|
||||
{`${issue?.issue_detail?.project_detail?.identifier}-${issue?.issue_detail?.sequence_id}`}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="duration-300"
|
||||
onClick={() => {
|
||||
const updatedBlocked = blockedIssue.filter((i) => i?.id !== issue?.id);
|
||||
|
||||
issuesService
|
||||
.deleteIssueRelation(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
relation.id,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-2 w-2" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
if (!user) return;
|
||||
|
||||
{/* duplicate */}
|
||||
<div>
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<CopyPlus height={16} width={16} className="text-custom-text-400" />
|
||||
<span className="text-sm text-custom-text-400">Duplicate</span>
|
||||
</div>
|
||||
<div>
|
||||
<DuplicateSelect
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{duplicateIssuesRelation &&
|
||||
duplicateIssuesRelation.map((relation) => (
|
||||
<div
|
||||
key={relation.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"
|
||||
>
|
||||
<span
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"issue",
|
||||
JSON.stringify({
|
||||
issue_id: relation.issue_detail?.id,
|
||||
project_id: relation.issue_detail?.project_detail.id,
|
||||
})
|
||||
)
|
||||
}
|
||||
className="flex items-center gap-1"
|
||||
issuesService.deleteIssueRelation(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
issue.id,
|
||||
user
|
||||
);
|
||||
|
||||
handleMutation({
|
||||
related_issues: updatedBlocked,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<CopyPlus height={10} width={10} />
|
||||
{`${relation?.issue_detail?.project_detail?.identifier}-${relation?.issue_detail?.sequence_id}`}
|
||||
</span>
|
||||
{!isArchive && !(memberRole.isGuest || memberRole.isViewer) && (
|
||||
<button
|
||||
type="button"
|
||||
className="duration-300"
|
||||
onClick={() => {
|
||||
if (memberRole.isGuest || memberRole.isViewer || !user) return;
|
||||
|
||||
issuesService
|
||||
.deleteIssueRelation(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
relation.id,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-2 w-2" />
|
||||
</button>
|
||||
)}
|
||||
<X className="h-2 w-2" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* relates to */}
|
||||
<div>
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<RelatedIcon height={16} width={16} color="rgb(var(--color-text-400))" />
|
||||
<span className="text-sm text-custom-text-400">Relates To</span>
|
||||
</div>
|
||||
<div>
|
||||
<RelatesSelect
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{relatedToIssueRelation &&
|
||||
relatedToIssueRelation.map((relation) => (
|
||||
<div
|
||||
key={relation.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"
|
||||
>
|
||||
<span
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"issue",
|
||||
JSON.stringify({
|
||||
issue_id: relation.issue_detail?.id,
|
||||
project_id: relation.issue_detail?.project_detail.id,
|
||||
})
|
||||
)
|
||||
}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<RelatedIcon height={10} width={10} />
|
||||
{`${relation?.issue_detail?.project_detail?.identifier}-${relation?.issue_detail?.sequence_id}`}
|
||||
</span>
|
||||
{!isArchive && !(memberRole.isGuest || memberRole.isViewer) && (
|
||||
<button
|
||||
type="button"
|
||||
className="duration-300"
|
||||
onClick={() => {
|
||||
if (memberRole.isGuest || memberRole.isViewer || !user) return;
|
||||
|
||||
issuesService
|
||||
.deleteIssueRelation(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
relation.id,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-2 w-2" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -508,11 +403,10 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value } }) => (
|
||||
<DateSelector
|
||||
placeholderText="Due date"
|
||||
value={value ?? undefined}
|
||||
<CustomDatePicker
|
||||
placeholder="Due date"
|
||||
value={value}
|
||||
wrapperClassName="w-full"
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
onChange={(val) =>
|
||||
submitChanges({
|
||||
target_date: val,
|
||||
@@ -527,46 +421,9 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<RectangleGroupIcon className="h-4 w-4 flex-shrink-0 text-custom-text-400" />
|
||||
<span className="text-sm text-custom-text-400">Module</span>
|
||||
</div>
|
||||
<div>
|
||||
<ModuleSelect
|
||||
value={watch("issue_module")}
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<ContrastIcon
|
||||
color="rgba(var(--color-text-400))"
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="text-sm text-custom-text-400">Cycle</span>
|
||||
</div>
|
||||
<div>
|
||||
<CycleSelect
|
||||
value={watch("issue_cycle")}
|
||||
disabled={isArchive || memberRole.isGuest || memberRole.isViewer}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<div className="mb-[6px]">
|
||||
<SecondaryButton
|
||||
type="button"
|
||||
onClick={() => setIsViewAllOpen((prev) => !prev)}
|
||||
|
||||
@@ -138,7 +138,6 @@ export const IssueWebViewForm: React.FC<Props> = (props) => {
|
||||
}
|
||||
noBorder={!isAllowed}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
if (!isAllowed) return;
|
||||
setShowAlert(true);
|
||||
setIsSubmitting("submitting");
|
||||
onChange(description_html);
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
// react
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import useDebounce from "hooks/use-debounce";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
|
||||
// components
|
||||
import { WebViewModal } from "components/web-view";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
import { Loader, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||
|
||||
// types
|
||||
import { ISearchIssueResponse, TProjectIssuesSearchParams } from "types";
|
||||
|
||||
type IssuesSelectBottomSheetProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: ISearchIssueResponse[]) => Promise<void>;
|
||||
searchParams: Partial<TProjectIssuesSearchParams>;
|
||||
singleSelect?: boolean;
|
||||
};
|
||||
|
||||
export const IssuesSelectBottomSheet: React.FC<IssuesSelectBottomSheetProps> = (props) => {
|
||||
const { isOpen, onClose, onSubmit, searchParams, singleSelect = false } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [selectedIssues, setSelectedIssues] = useState<ISearchIssueResponse[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||
|
||||
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setSearchTerm("");
|
||||
setSelectedIssues([]);
|
||||
setIsWorkspaceLevel(false);
|
||||
};
|
||||
|
||||
const handleSelect = async (data: ISearchIssueResponse[]) => {
|
||||
if (!user || !workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
await onSubmit(data).finally(() => {
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
|
||||
handleClose();
|
||||
|
||||
console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "success",
|
||||
message: `Issue${data.length > 1 ? "s" : ""} added successfully.`,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
setIsSearching(true);
|
||||
|
||||
projectService
|
||||
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
|
||||
search: debouncedSearchTerm,
|
||||
...searchParams,
|
||||
issue_id: issueId.toString(),
|
||||
workspace_search: isWorkspaceLevel,
|
||||
})
|
||||
.then((res) => setIssues(res))
|
||||
.finally(() => setIsSearching(false));
|
||||
}, [
|
||||
debouncedSearchTerm,
|
||||
isOpen,
|
||||
isWorkspaceLevel,
|
||||
issueId,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
searchParams,
|
||||
]);
|
||||
|
||||
return (
|
||||
<WebViewModal isOpen={isOpen} onClose={handleClose} modalTitle="Select issue">
|
||||
{!isSearching && issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||
<LayerDiagonalIcon height="52" width="52" />
|
||||
<h3 className="text-custom-text-200">
|
||||
No issues found. Create a new issue with{" "}
|
||||
<pre className="inline rounded bg-custom-background-80 px-2 py-1 text-sm">C</pre>.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`flex-shrink-0 flex items-center gap-1 text-xs pb-3 cursor-pointer ${
|
||||
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<ToggleSwitch
|
||||
value={isWorkspaceLevel}
|
||||
onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
Workspace Level
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isSearching && (
|
||||
<Loader className="space-y-3 p-3">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
|
||||
{!isSearching && (
|
||||
<WebViewModal.Options
|
||||
options={issues.map((issue) => ({
|
||||
value: issue.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state__color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs">
|
||||
{issue.project__identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
{issue.name}
|
||||
</div>
|
||||
),
|
||||
checked: selectedIssues.some((i) => i.id === issue.id),
|
||||
onClick() {
|
||||
if (singleSelect) {
|
||||
handleSelect([issue]);
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedIssues.some((i) => i.id === issue.id)) {
|
||||
setSelectedIssues(selectedIssues.filter((i) => i.id !== issue.id));
|
||||
} else {
|
||||
setSelectedIssues([...selectedIssues, issue]);
|
||||
}
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedIssues.length > 0 && (
|
||||
<WebViewModal.Footer className="flex items-center justify-end gap-2">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
handleSelect(selectedIssues);
|
||||
}}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Adding..." : "Add selected issues"}
|
||||
</PrimaryButton>
|
||||
</WebViewModal.Footer>
|
||||
)}
|
||||
</WebViewModal>
|
||||
);
|
||||
};
|
||||
@@ -47,7 +47,7 @@ export const AssigneeSelect: React.FC<Props> = (props) => {
|
||||
<>
|
||||
<WebViewModal
|
||||
isOpen={isOpen}
|
||||
modalTitle="Select assignees"
|
||||
modalTitle="Select state"
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
@@ -74,20 +74,20 @@ export const AssigneeSelect: React.FC<Props> = (props) => {
|
||||
disabled={disabled}
|
||||
onClick={() => setIsOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5"
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
{value && value.length > 0 && Array.isArray(value) ? (
|
||||
<div className="-my-0.5 flex items-center gap-2">
|
||||
<Avatar user={selectedAssignees?.[0].member} />
|
||||
<span className="text-custom-text-200 text-xs">
|
||||
<span className="text-custom-text-100 text-xs">
|
||||
{selectedAssignees?.length} Assignees
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-custom-text-200">No assignees</span>
|
||||
"No assignees"
|
||||
)}
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { IssuesSelectBottomSheet } from "components/web-view";
|
||||
|
||||
// types
|
||||
import type { IIssue, BlockeIssueDetail, ISearchIssueResponse } from "types";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const BlockedBySelect: React.FC<Props> = (props) => {
|
||||
const { disabled = false } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { watch } = useFormContext<IIssue>();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||
|
||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || !user || disabled) return;
|
||||
|
||||
if (data.length === 0)
|
||||
return console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: "Please select at least one issue.",
|
||||
})
|
||||
);
|
||||
|
||||
const selectedIssues: { blocked_issue_detail: BlockeIssueDetail }[] = data.map((i) => ({
|
||||
blocked_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,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const relatedIssues = watch("related_issues");
|
||||
|
||||
await issuesService
|
||||
.createIssueRelation(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issueId.toString(),
|
||||
user,
|
||||
{
|
||||
related_list: [
|
||||
...selectedIssues.map((issue) => ({
|
||||
issue: issueId as string,
|
||||
relation_type: "blocked_by" as const,
|
||||
issue_detail: issue.blocked_issue_detail,
|
||||
related_issue: issue.blocked_issue_detail.id,
|
||||
})),
|
||||
],
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
mutate(ISSUE_DETAILS(issueId as string), (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
return {
|
||||
...prevData,
|
||||
related_issues: [...relatedIssues, ...response],
|
||||
};
|
||||
});
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
|
||||
setIsBlockedModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuesSelectBottomSheet
|
||||
isOpen={isBlockedModalOpen}
|
||||
onSubmit={onSubmit}
|
||||
onClose={() => setIsBlockedModalOpen(false)}
|
||||
searchParams={{ issue_relation: true }}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBlockedModalOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
|
||||
// types
|
||||
import { BlockeIssueDetail, ISearchIssueResponse } from "types";
|
||||
|
||||
type Props = {
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const BlockedSelect: React.FC<Props> = (props) => {
|
||||
const { value, onChange, disabled = false } = props;
|
||||
|
||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { issueId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
onChange([...(value || []), ...selectedIssues]);
|
||||
|
||||
setIsBlockedModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
isOpen={isBlockedModalOpen}
|
||||
handleClose={() => setIsBlockedModalOpen(false)}
|
||||
searchParams={{ issue_relation: true, issue_id: issueId!.toString() }}
|
||||
handleOnSubmit={onSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBlockedModalOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { IssuesSelectBottomSheet } from "components/web-view";
|
||||
|
||||
// types
|
||||
import { BlockeIssueDetail, ISearchIssueResponse, IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const BlockerSelect: React.FC<Props> = (props) => {
|
||||
const { disabled = false } = props;
|
||||
|
||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { watch } = useFormContext<IIssue>();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||
if (disabled) return;
|
||||
|
||||
if (data.length === 0)
|
||||
return console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: "Please select at least one issue.",
|
||||
})
|
||||
);
|
||||
|
||||
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 (!workspaceSlug || !projectId || !issueId || !user) return;
|
||||
|
||||
const blockerIssue =
|
||||
watch("issue_relations")?.filter((i) => i.relation_type === "blocked_by") || [];
|
||||
|
||||
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,
|
||||
})),
|
||||
],
|
||||
relation: "blocking",
|
||||
})
|
||||
.then((response) => {
|
||||
mutate(ISSUE_DETAILS(issueId as string), (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
return {
|
||||
...prevData,
|
||||
issue_relations: [
|
||||
...blockerIssue,
|
||||
...(response ?? []).map((i: any) => ({
|
||||
id: i.id,
|
||||
relation_type: i.relation_type,
|
||||
issue_detail: i.issue_detail,
|
||||
issue: i.related_issue,
|
||||
})),
|
||||
],
|
||||
};
|
||||
});
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
|
||||
setIsBlockerModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuesSelectBottomSheet
|
||||
isOpen={isBlockerModalOpen}
|
||||
onClose={() => setIsBlockerModalOpen(false)}
|
||||
onSubmit={onSubmit}
|
||||
searchParams={{ issue_relation: true }}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBlockerModalOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
|
||||
// types
|
||||
import { BlockeIssueDetail, ISearchIssueResponse } from "types";
|
||||
|
||||
type Props = {
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const BlockerSelect: React.FC<Props> = (props) => {
|
||||
const { value, onChange, disabled = false } = props;
|
||||
|
||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { issueId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
onChange([...(value || []), ...selectedIssues]);
|
||||
|
||||
setIsBlockerModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
isOpen={isBlockerModalOpen}
|
||||
handleClose={() => setIsBlockerModalOpen(false)}
|
||||
searchParams={{ issue_relation: true, issue_id: issueId!.toString() }}
|
||||
handleOnSubmit={onSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBlockerModalOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,152 +0,0 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import cyclesService from "services/cycles.service";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
// fetch keys
|
||||
import {
|
||||
ISSUE_DETAILS,
|
||||
INCOMPLETE_CYCLES_LIST,
|
||||
CYCLE_ISSUES,
|
||||
PROJECT_ISSUES_ACTIVITY,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { WebViewModal } from "components/web-view";
|
||||
|
||||
// types
|
||||
import { ICycle, IIssueCycle } from "types";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
value?: IIssueCycle | null;
|
||||
};
|
||||
|
||||
export const CycleSelect: React.FC<Props> = (props) => {
|
||||
const { disabled = false, value } = props;
|
||||
|
||||
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { data: incompleteCycles } = useSWR(
|
||||
workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cyclesService.getCyclesWithParams(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
"incomplete"
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const handleCycleChange = (cycleDetails: ICycle) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || disabled) return;
|
||||
|
||||
issuesService
|
||||
.addIssueToCycle(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
cycleDetails.id,
|
||||
{
|
||||
issues: [issueId.toString()],
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
};
|
||||
|
||||
const removeIssueFromCycle = (bridgeId?: string, cycleId?: string) => {
|
||||
if (!workspaceSlug || !projectId || !bridgeId || !cycleId || disabled) return;
|
||||
|
||||
mutate(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prev) => {
|
||||
if (!prev) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
issue_cycle: null,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
issuesService
|
||||
.removeIssueFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId, bridgeId)
|
||||
.then(() => {
|
||||
mutate(CYCLE_ISSUES(cycleId));
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebViewModal
|
||||
isOpen={isBottomSheetOpen}
|
||||
onClose={() => setIsBottomSheetOpen(false)}
|
||||
modalTitle="Select Module"
|
||||
>
|
||||
<WebViewModal.Options
|
||||
options={[
|
||||
...(incompleteCycles ?? []).map((cycle) => ({
|
||||
checked: cycle.id === value?.cycle,
|
||||
label: cycle.name,
|
||||
value: cycle.id,
|
||||
onClick: () => {
|
||||
handleCycleChange(cycle);
|
||||
setIsBottomSheetOpen(false);
|
||||
},
|
||||
})),
|
||||
{
|
||||
checked: !value,
|
||||
label: "None",
|
||||
onClick: () => {
|
||||
setIsBottomSheetOpen(false);
|
||||
removeIssueFromCycle(value?.id, value?.cycle);
|
||||
},
|
||||
value: "none",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</WebViewModal>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBottomSheetOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">{value?.cycle_detail.name ?? "Select cycle"}</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { IssuesSelectBottomSheet } from "components/web-view";
|
||||
|
||||
// types
|
||||
import { BlockeIssueDetail, ISearchIssueResponse } from "types";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const DuplicateSelect: React.FC<Props> = (props) => {
|
||||
const { disabled = false } = props;
|
||||
|
||||
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || !user || disabled) return;
|
||||
|
||||
if (data.length === 0)
|
||||
return console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: "Please select at least one issue.",
|
||||
})
|
||||
);
|
||||
|
||||
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.toString(),
|
||||
projectId.toString(),
|
||||
issueId.toString(),
|
||||
user,
|
||||
{
|
||||
related_list: [
|
||||
...selectedIssues.map((issue) => ({
|
||||
issue: issueId as string,
|
||||
issue_detail: issue.blocker_issue_detail,
|
||||
related_issue: issue.blocker_issue_detail.id,
|
||||
relation_type: "duplicate" as const,
|
||||
})),
|
||||
],
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
|
||||
setIsBottomSheetOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuesSelectBottomSheet
|
||||
isOpen={isBottomSheetOpen}
|
||||
onClose={() => setIsBottomSheetOpen(false)}
|
||||
onSubmit={onSubmit}
|
||||
searchParams={{ issue_relation: true }}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBottomSheetOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,147 +0,0 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import modulesService from "services/modules.service";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
// fetch keys
|
||||
import {
|
||||
ISSUE_DETAILS,
|
||||
MODULE_LIST,
|
||||
MODULE_ISSUES,
|
||||
PROJECT_ISSUES_ACTIVITY,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { WebViewModal } from "components/web-view";
|
||||
|
||||
// types
|
||||
import { IModule, IIssueModule } from "types";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
value?: IIssueModule | null;
|
||||
};
|
||||
|
||||
export const ModuleSelect: React.FC<Props> = (props) => {
|
||||
const { disabled = false, value } = props;
|
||||
|
||||
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { data: modules } = useSWR(
|
||||
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => modulesService.getModules(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const handleModuleChange = (moduleDetail: IModule) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || disabled) return;
|
||||
|
||||
modulesService
|
||||
.addIssuesToModule(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
moduleDetail.id,
|
||||
{
|
||||
issues: [issueId.toString()],
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId.toString()));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
};
|
||||
|
||||
const removeIssueFromModule = (bridgeId?: string, moduleId?: string) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId || !bridgeId || disabled) return;
|
||||
|
||||
mutate(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
issue_module: null,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
modulesService
|
||||
.removeIssueFromModule(workspaceSlug as string, projectId as string, moduleId, bridgeId)
|
||||
.then(() => {
|
||||
mutate(MODULE_ISSUES(moduleId));
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebViewModal
|
||||
isOpen={isBottomSheetOpen}
|
||||
onClose={() => setIsBottomSheetOpen(false)}
|
||||
modalTitle="Select Module"
|
||||
>
|
||||
<WebViewModal.Options
|
||||
options={[
|
||||
...(modules ?? []).map((mod) => ({
|
||||
checked: mod.id === value?.module,
|
||||
label: mod.name,
|
||||
value: mod.id,
|
||||
onClick: () => {
|
||||
handleModuleChange(mod);
|
||||
setIsBottomSheetOpen(false);
|
||||
},
|
||||
})),
|
||||
{
|
||||
checked: !value,
|
||||
label: "None",
|
||||
onClick: () => {
|
||||
setIsBottomSheetOpen(false);
|
||||
removeIssueFromModule(value?.id, value?.module);
|
||||
},
|
||||
value: "none",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</WebViewModal>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBottomSheetOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">
|
||||
{value?.module_detail?.name ?? "Select module"}
|
||||
</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -14,10 +14,7 @@ import issuesService from "services/issues.service";
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
// components
|
||||
import { IssuesSelectBottomSheet } from "components/web-view";
|
||||
|
||||
// icons
|
||||
import { ChevronDown, X } from "lucide-react";
|
||||
import { ParentIssuesListModal } from "components/issues";
|
||||
|
||||
// types
|
||||
import { ISearchIssueResponse } from "types";
|
||||
@@ -29,7 +26,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const ParentSelect: React.FC<Props> = (props) => {
|
||||
const { onChange, disabled = false } = props;
|
||||
const { value, onChange, disabled = false } = props;
|
||||
|
||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||
@@ -45,67 +42,35 @@ export const ParentSelect: React.FC<Props> = (props) => {
|
||||
: null
|
||||
);
|
||||
|
||||
const parentIssueResult = selectedParentIssue
|
||||
? `${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
||||
: issueDetails?.parent
|
||||
? `${issueDetails.parent_detail?.project_detail.identifier}-${issueDetails.parent_detail?.sequence_id}`
|
||||
: null; // defaults to null
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuesSelectBottomSheet
|
||||
<ParentIssuesListModal
|
||||
isOpen={isParentModalOpen}
|
||||
onClose={() => setIsParentModalOpen(false)}
|
||||
singleSelect
|
||||
onSubmit={async (issues) => {
|
||||
if (disabled) return;
|
||||
const issue = issues[0];
|
||||
handleClose={() => setIsParentModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
onChange(issue.id);
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
searchParams={{
|
||||
parent: true,
|
||||
issue_id: issueId as string,
|
||||
}}
|
||||
issueId={issueId as string}
|
||||
projectId={projectId as string}
|
||||
/>
|
||||
|
||||
{parentIssueResult ? (
|
||||
<div className="flex justify-between items-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsParentModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span>{parentIssueResult}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className="pr-2.5"
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
setSelectedParentIssue(null);
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setIsParentModalOpen(true);
|
||||
}}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5"
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsParentModalOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
{selectedParentIssue && issueDetails?.parent ? (
|
||||
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
||||
) : !selectedParentIssue && issueDetails?.parent ? (
|
||||
`${issueDetails.parent_detail?.project_detail.identifier}-${issueDetails.parent_detail?.sequence_id}`
|
||||
) : (
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -74,13 +74,11 @@ export const PrioritySelect: React.FC<Props> = (props) => {
|
||||
disabled={disabled}
|
||||
onClick={() => setIsOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5"
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">
|
||||
{value ? capitalizeFirstLetter(value) : "None"}
|
||||
</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
{value ? capitalizeFirstLetter(value) : "None"}
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { IssuesSelectBottomSheet } from "components/web-view";
|
||||
|
||||
// types
|
||||
import { BlockeIssueDetail, ISearchIssueResponse } from "types";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const RelatesSelect: React.FC<Props> = (props) => {
|
||||
const { disabled = false } = props;
|
||||
|
||||
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || !user || disabled) return;
|
||||
|
||||
if (data.length === 0)
|
||||
return console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: "Please select at least one issue.",
|
||||
})
|
||||
);
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
issuesService
|
||||
.createIssueRelation(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issueId.toString(),
|
||||
user,
|
||||
{
|
||||
related_list: [
|
||||
...selectedIssues.map((issue) => ({
|
||||
issue: issueId as string,
|
||||
issue_detail: issue.blocker_issue_detail,
|
||||
related_issue: issue.blocker_issue_detail.id,
|
||||
relation_type: "relates_to" as const,
|
||||
})),
|
||||
],
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
});
|
||||
|
||||
setIsBottomSheetOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuesSelectBottomSheet
|
||||
isOpen={isBottomSheetOpen}
|
||||
onClose={() => setIsBottomSheetOpen(false)}
|
||||
onSubmit={onSubmit}
|
||||
searchParams={{ issue_relation: true }}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBottomSheetOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -78,11 +78,11 @@ export const StateSelect: React.FC<Props> = (props) => {
|
||||
disabled={disabled}
|
||||
onClick={() => setIsOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5"
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">{selectedState?.name || "Select a state"}</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
{selectedState?.name || "Select a state"}
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export const Spinner: React.FC = () => (
|
||||
<div className="animate-spin duration-[2500ms]">
|
||||
<img src="/web-view-spinner.png" alt="spinner" />
|
||||
</div>
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
@@ -8,7 +8,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// icons
|
||||
import { X, PlusIcon } from "lucide-react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
@@ -21,12 +21,10 @@ import useUser from "hooks/use-user";
|
||||
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
import { IIssue } from "types";
|
||||
|
||||
// components
|
||||
import { Label, IssuesSelectBottomSheet, DeleteConfirmation } from "components/web-view";
|
||||
|
||||
// types
|
||||
import { IIssue, ISearchIssueResponse } from "types";
|
||||
import { Label } from "components/web-view";
|
||||
|
||||
type Props = {
|
||||
issueDetails?: IIssue;
|
||||
@@ -36,12 +34,7 @@ export const SubIssueList: React.FC<Props> = (props) => {
|
||||
const { issueDetails } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const isArchive = Boolean(router.query.archive);
|
||||
|
||||
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
|
||||
const [issueSelectedForDelete, setIssueSelectedForDelete] = useState<IIssue | null>(null);
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
@@ -53,8 +46,8 @@ export const SubIssueList: React.FC<Props> = (props) => {
|
||||
: null
|
||||
);
|
||||
|
||||
const handleSubIssueRemove = (issue: IIssue | null) => {
|
||||
if (!workspaceSlug || !issueDetails || !user || !issue) return;
|
||||
const handleSubIssueRemove = (issue: any) => {
|
||||
if (!workspaceSlug || !issueDetails || !user) return;
|
||||
|
||||
mutate(
|
||||
SUB_ISSUES(issueDetails.id),
|
||||
@@ -79,40 +72,8 @@ export const SubIssueList: React.FC<Props> = (props) => {
|
||||
.finally(() => mutate(SUB_ISSUES(issueDetails.id)));
|
||||
};
|
||||
|
||||
const addAsSubIssueFromExistingIssues = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || isArchive) return;
|
||||
|
||||
const payload = {
|
||||
sub_issue_ids: data.map((i) => i.id),
|
||||
};
|
||||
await issuesService
|
||||
.addSubIssues(workspaceSlug.toString(), projectId.toString(), issueId.toString(), payload)
|
||||
.finally(() => {
|
||||
mutate(SUB_ISSUES(issueId.toString()));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IssuesSelectBottomSheet
|
||||
isOpen={isBottomSheetOpen}
|
||||
onClose={() => setIsBottomSheetOpen(false)}
|
||||
onSubmit={addAsSubIssueFromExistingIssues}
|
||||
searchParams={{ sub_issue: true, issue_id: issueId as string }}
|
||||
/>
|
||||
|
||||
<DeleteConfirmation
|
||||
title="Remove sub issue"
|
||||
content="Are you sure you want to remove this sub issue?"
|
||||
isOpen={!!issueSelectedForDelete}
|
||||
onCancel={() => setIssueSelectedForDelete(null)}
|
||||
onConfirm={() => {
|
||||
if (isArchive) return;
|
||||
setIssueSelectedForDelete(null);
|
||||
handleSubIssueRemove(issueSelectedForDelete);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Label>Sub Issues</Label>
|
||||
<div className="p-3 border border-custom-border-200 rounded-[4px]">
|
||||
{!subIssuesResponse && (
|
||||
@@ -136,28 +97,12 @@ export const SubIssueList: React.FC<Props> = (props) => {
|
||||
</p>
|
||||
<p className="text-sm font-normal">{subIssue.name}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isArchive}
|
||||
onClick={() => {
|
||||
if (isArchive) return;
|
||||
setIssueSelectedForDelete(subIssue);
|
||||
}}
|
||||
>
|
||||
<button type="button" onClick={() => handleSubIssueRemove(subIssue)}>
|
||||
<X className="w-[18px] h-[18px] text-custom-text-400" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isArchive}
|
||||
onClick={() => setIsBottomSheetOpen(true)}
|
||||
className="flex items-center gap-x-1 mt-3"
|
||||
>
|
||||
<PlusIcon className="w-[18px] h-[18px] text-custom-text-400" />
|
||||
<p className="text-sm text-custom-text-400">Add sub issue</p>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ export const WebViewModal = (props: Props) => {
|
||||
<XMarkIcon className="w-6 h-6 text-custom-text-200" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col mt-6 h-full max-h-[70vh]">{children}</div>
|
||||
<div className="mt-6 max-h-60 overflow-auto">{children}</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@ export const WebViewModal = (props: Props) => {
|
||||
|
||||
type OptionsProps = {
|
||||
options: Array<{
|
||||
label: string | React.ReactNode;
|
||||
label: string;
|
||||
value: string | null;
|
||||
checked: boolean;
|
||||
icon?: any;
|
||||
@@ -84,14 +84,14 @@ type OptionsProps = {
|
||||
};
|
||||
|
||||
const Options: React.FC<OptionsProps> = ({ options }) => (
|
||||
<div className="divide-y divide-custom-border-300 flex-1 overflow-auto">
|
||||
<div className="divide-y">
|
||||
{options.map((option) => (
|
||||
<div key={option.value} className="flex items-center justify-between gap-2 py-[14px]">
|
||||
<div onClick={option.onClick} className="flex items-center gap-x-2 w-full">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={option.checked}
|
||||
readOnly
|
||||
onChange={option.onClick}
|
||||
className="rounded-full border border-custom-border-200 bg-custom-background-100 w-4 h-4"
|
||||
/>
|
||||
|
||||
@@ -104,16 +104,5 @@ const Options: React.FC<OptionsProps> = ({ options }) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
type FooterProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Footer: React.FC<FooterProps> = ({ children, className }) => (
|
||||
<div className={`mt-2 flex-shrink-0 ${className ? className : ""}`}>{children}</div>
|
||||
);
|
||||
|
||||
WebViewModal.Options = Options;
|
||||
WebViewModal.Footer = Footer;
|
||||
WebViewModal.Options.displayName = "WebViewModal.Options";
|
||||
WebViewModal.Footer.displayName = "WebViewModal.Footer";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CURRENT_USER } from "constants/fetch-keys";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
// ui
|
||||
import { Spinner } from "components/web-view";
|
||||
import { Spinner } from "components/ui";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -40,6 +40,7 @@ const WebViewLayout: React.FC<Props> = ({ children, fullScreen = true }) => {
|
||||
return (
|
||||
<div className="h-screen grid place-items-center p-4">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<h3 className="text-xl">Loading your profile...</h3>
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,7 @@ import WebViewLayout from "layouts/web-view-layout";
|
||||
|
||||
// components
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
import { PrimaryButton } from "components/ui";
|
||||
import { Spinner } from "components/web-view";
|
||||
import { PrimaryButton, Spinner } from "components/ui";
|
||||
|
||||
const Editor: NextPage = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -42,13 +41,13 @@ const Editor: NextPage = () => {
|
||||
}, [isEditable, setValue, router]);
|
||||
|
||||
return (
|
||||
<WebViewLayout fullScreen>
|
||||
<WebViewLayout fullScreen={isLoading}>
|
||||
{isLoading ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col justify-between">
|
||||
<>
|
||||
<Controller
|
||||
name="data_html"
|
||||
control={control}
|
||||
@@ -65,7 +64,8 @@ const Editor: NextPage = () => {
|
||||
editable={isEditable}
|
||||
noBorder={true}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
customClassName="h-full shadow-sm overflow-auto"
|
||||
debouncedUpdatesEnabled={true}
|
||||
customClassName="min-h-[150px] shadow-sm"
|
||||
editorContentCustomClassNames="pb-9"
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
@@ -77,7 +77,7 @@ const Editor: NextPage = () => {
|
||||
/>
|
||||
{isEditable && (
|
||||
<PrimaryButton
|
||||
className="mt-4 w-[calc(100%-30px)] h-[45px] mx-[15px] text-[17px] my-[15px]"
|
||||
className="mt-4 w-[calc(100%-30px)] h-[45px] mx-[15px] text-[17px]"
|
||||
onClick={() => {
|
||||
console.log(
|
||||
"submitted",
|
||||
@@ -90,7 +90,7 @@ const Editor: NextPage = () => {
|
||||
Submit
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</WebViewLayout>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// react hook forms
|
||||
import { useFormContext, useForm, FormProvider } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
@@ -23,6 +23,9 @@ import useProjectMembers from "hooks/use-project-members";
|
||||
// layouts
|
||||
import WebViewLayout from "layouts/web-view-layout";
|
||||
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
|
||||
// components
|
||||
import {
|
||||
IssueWebViewForm,
|
||||
@@ -31,58 +34,46 @@ import {
|
||||
IssuePropertiesDetail,
|
||||
IssueLinks,
|
||||
IssueActivity,
|
||||
Spinner,
|
||||
} from "components/web-view";
|
||||
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
|
||||
const MobileWebViewIssueDetail_ = () => {
|
||||
const MobileWebViewIssueDetail = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const isArchive = Boolean(router.query.archive);
|
||||
|
||||
const memberRole = useProjectMembers(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
!!workspaceSlug && !!projectId
|
||||
);
|
||||
|
||||
const isAllowed = Boolean((memberRole.isMember || memberRole.isOwner) && !isArchive);
|
||||
const isAllowed = Boolean(memberRole.isMember || memberRole.isOwner);
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const formContext = useFormContext<IIssue>();
|
||||
const { register, handleSubmit, control, watch, reset } = formContext;
|
||||
const { register, control, reset, handleSubmit, watch } = useForm<IIssue>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
description_html: "",
|
||||
state: "",
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: issue,
|
||||
mutate: mutateIssue,
|
||||
data: issueDetails,
|
||||
mutate: mutateIssueDetails,
|
||||
error,
|
||||
} = useSWR(
|
||||
workspaceSlug && projectId && issueId && !isArchive ? ISSUE_DETAILS(issueId.toString()) : null,
|
||||
workspaceSlug && projectId && issueId && !isArchive
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () =>
|
||||
issuesService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: archiveIssueDetails, mutate: mutateaArchiveIssue } = useSWR<IIssue | undefined>(
|
||||
workspaceSlug && projectId && issueId && isArchive ? ISSUE_DETAILS(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId && isArchive
|
||||
? () =>
|
||||
issuesService.retrieveArchivedIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issueId.toString()
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const issueDetails = isArchive ? archiveIssueDetails : issue;
|
||||
const mutateIssueDetails = isArchive ? mutateaArchiveIssue : mutateIssue;
|
||||
|
||||
useEffect(() => {
|
||||
if (!issueDetails) return;
|
||||
reset({
|
||||
@@ -141,6 +132,7 @@ const MobileWebViewIssueDetail_ = () => {
|
||||
<div className="px-4 py-2 h-full">
|
||||
<div className="h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</WebViewLayout>
|
||||
@@ -155,10 +147,6 @@ const MobileWebViewIssueDetail_ = () => {
|
||||
|
||||
return (
|
||||
<WebViewLayout>
|
||||
{isArchive && (
|
||||
<div className="w-full h-screen top-0 left-0 fixed z-50 bg-white/20 pointer-events-none" />
|
||||
)}
|
||||
|
||||
<div className="px-6 py-2 h-full overflow-auto space-y-3">
|
||||
<IssueWebViewForm
|
||||
isAllowed={isAllowed}
|
||||
@@ -172,7 +160,7 @@ const MobileWebViewIssueDetail_ = () => {
|
||||
|
||||
<SubIssueList issueDetails={issueDetails!} />
|
||||
|
||||
<IssuePropertiesDetail submitChanges={submitChanges} />
|
||||
<IssuePropertiesDetail control={control} submitChanges={submitChanges} />
|
||||
|
||||
<IssueAttachments allowed={isAllowed} />
|
||||
|
||||
@@ -184,14 +172,4 @@ const MobileWebViewIssueDetail_ = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const MobileWebViewIssueDetail = () => {
|
||||
const methods = useForm();
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<MobileWebViewIssueDetail_ />
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileWebViewIssueDetail;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -359,114 +359,3 @@ body {
|
||||
.disable-scroll {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
div.web-view-spinner {
|
||||
position: relative;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
display: inline-block;
|
||||
margin-left: 50%;
|
||||
margin-right: 50%;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
div.web-view-spinner div {
|
||||
width: 6%;
|
||||
height: 16%;
|
||||
background: rgb(var(--color-text-400));
|
||||
position: absolute;
|
||||
left: 49%;
|
||||
top: 43%;
|
||||
opacity: 0;
|
||||
border-radius: 50px;
|
||||
-webkit-border-radius: 50px;
|
||||
box-shadow: 0 0 3px rgba(0,0,0,0.2);
|
||||
-webkit-box-shadow: 0 0 3px rgba(0,0,0,0.2);
|
||||
animation: fade 1s linear infinite;
|
||||
-webkit-animation: fade 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
from {opacity: 1;}
|
||||
to {opacity: 0.25;}
|
||||
}
|
||||
@-webkit-keyframes fade {
|
||||
from {opacity: 1;}
|
||||
to {opacity: 0.25;}
|
||||
}
|
||||
|
||||
div.web-view-spinner div.bar1 {
|
||||
transform: rotate(0deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(0deg) translate(0, -130%);
|
||||
animation-delay: 0s;
|
||||
-webkit-animation-delay: 0s;
|
||||
}
|
||||
|
||||
div.web-view-spinner div.bar2 {
|
||||
transform:rotate(30deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(30deg) translate(0, -130%);
|
||||
animation-delay: -0.9167s;
|
||||
-webkit-animation-delay: -0.9167s;
|
||||
}
|
||||
|
||||
div.web-view-spinner div.bar3 {
|
||||
transform:rotate(60deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(60deg) translate(0, -130%);
|
||||
animation-delay: -0.833s;
|
||||
-webkit-animation-delay: -0.833s;
|
||||
}
|
||||
div.web-view-spinner div.bar4 {
|
||||
transform:rotate(90deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(90deg) translate(0, -130%);
|
||||
animation-delay: -0.7497s;
|
||||
-webkit-animation-delay: -0.7497s;
|
||||
}
|
||||
div.web-view-spinner div.bar5 {
|
||||
transform:rotate(120deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(120deg) translate(0, -130%);
|
||||
animation-delay: -0.667s;
|
||||
-webkit-animation-delay: -0.667s;
|
||||
}
|
||||
div.web-view-spinner div.bar6 {
|
||||
transform:rotate(150deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(150deg) translate(0, -130%);
|
||||
animation-delay: -0.5837s;
|
||||
-webkit-animation-delay: -0.5837s;
|
||||
}
|
||||
div.web-view-spinner div.bar7 {
|
||||
transform:rotate(180deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(180deg) translate(0, -130%);
|
||||
animation-delay: -0.5s;
|
||||
-webkit-animation-delay: -0.5s;
|
||||
}
|
||||
div.web-view-spinner div.bar8 {
|
||||
transform:rotate(210deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(210deg) translate(0, -130%);
|
||||
animation-delay: -0.4167s;
|
||||
-webkit-animation-delay: -0.4167s;
|
||||
}
|
||||
div.web-view-spinner div.bar9 {
|
||||
transform:rotate(240deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(240deg) translate(0, -130%);
|
||||
animation-delay: -0.333s;
|
||||
-webkit-animation-delay: -0.333s;
|
||||
}
|
||||
div.web-view-spinner div.bar10 {
|
||||
transform:rotate(270deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(270deg) translate(0, -130%);
|
||||
animation-delay: -0.2497s;
|
||||
-webkit-animation-delay: -0.2497s;
|
||||
}
|
||||
div.web-view-spinner div.bar11 {
|
||||
transform:rotate(300deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(300deg) translate(0, -130%);
|
||||
animation-delay: -0.167s;
|
||||
-webkit-animation-delay: -0.167s;
|
||||
}
|
||||
div.web-view-spinner div.bar12 {
|
||||
transform:rotate(330deg) translate(0, -130%);
|
||||
-webkit-transform:rotate(330deg) translate(0, -130%);
|
||||
animation-delay: -0.0833s;
|
||||
-webkit-animation-delay: -0.0833s;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user