Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ab6621be3 |
+3
-3
@@ -39,10 +39,10 @@ jobs:
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaSpace
|
||||
id: metaDeploy
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
file: ./space/Dockerfile.space
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaSpace.outputs.tags }}
|
||||
tags: ${{ steps.metaDeploy.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -1,79 +0,0 @@
|
||||
name: Create PR in Plane EE Repository to sync the changes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
create_pr:
|
||||
# Only run the job when a PR is merged
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check SOURCE_REPO
|
||||
id: check_repo
|
||||
env:
|
||||
SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }}
|
||||
run: |
|
||||
echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)"
|
||||
|
||||
- name: Checkout Code
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Branch Name
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
run: |
|
||||
echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup GH CLI
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
run: |
|
||||
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||
sudo apt update
|
||||
sudo apt install gh -y
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
run: |
|
||||
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}"
|
||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||
|
||||
git checkout $SOURCE_BRANCH
|
||||
git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target $SOURCE_BRANCH:$SOURCE_BRANCH
|
||||
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_BODY="${{ github.event.pull_request.body }}"
|
||||
|
||||
# Remove double quotes
|
||||
PR_TITLE_CLEANED="${PR_TITLE//\"/}"
|
||||
PR_BODY_CLEANED="${PR_BODY//\"/}"
|
||||
|
||||
# Construct PR_BODY_CONTENT using a here-document
|
||||
PR_BODY_CONTENT=$(cat <<EOF
|
||||
$PR_BODY_CLEANED
|
||||
EOF
|
||||
)
|
||||
|
||||
gh pr create \
|
||||
--base $TARGET_BRANCH \
|
||||
--head $SOURCE_BRANCH \
|
||||
--title "[SYNC] $PR_TITLE_CLEANED" \
|
||||
--body "$PR_BODY_CONTENT" \
|
||||
--repo $TARGET_REPO
|
||||
@@ -54,7 +54,7 @@ chmod +x setup.sh
|
||||
- Run setup.sh
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
./setup.sh http://localhost
|
||||
```
|
||||
|
||||
> If running in a cloud env replace localhost with public facing IP address of the VM
|
||||
|
||||
+1
-12
@@ -1,7 +1,7 @@
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
DEBUG=0
|
||||
DJANGO_SETTINGS_MODULE="plane.settings.production"
|
||||
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
|
||||
|
||||
# Error logs
|
||||
SENTRY_DSN=""
|
||||
@@ -59,14 +59,3 @@ DEFAULT_PASSWORD="password123"
|
||||
|
||||
# SignUps
|
||||
ENABLE_SIGNUP="1"
|
||||
|
||||
|
||||
# Enable Email/Password Signup
|
||||
ENABLE_EMAIL_PASSWORD="1"
|
||||
|
||||
# Enable Magic link Login
|
||||
ENABLE_MAGIC_LINK_LOGIN="0"
|
||||
|
||||
# Email redirections and minio domain settings
|
||||
WEB_URL="http://localhost"
|
||||
|
||||
|
||||
@@ -58,17 +58,8 @@ class WorkspaceEntityPermission(BasePermission):
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
## Safe Methods -> Handle the filtering logic in queryset
|
||||
if request.method in SAFE_METHODS:
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
).exists()
|
||||
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__in=[Owner, Admin],
|
||||
member=request.user, workspace__slug=view.workspace_slug
|
||||
).exists()
|
||||
|
||||
|
||||
|
||||
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",
|
||||
),
|
||||
]
|
||||
@@ -17,7 +17,6 @@ from .project import (
|
||||
ProjectMemberEndpoint,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
LeaveProjectEndpoint,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
)
|
||||
from .user import (
|
||||
UserEndpoint,
|
||||
@@ -57,7 +56,7 @@ from .workspace import (
|
||||
LeaveWorkspaceEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet
|
||||
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
from .cycle import (
|
||||
CycleViewSet,
|
||||
CycleIssueViewSet,
|
||||
@@ -148,13 +147,16 @@ from .page import (
|
||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||
|
||||
|
||||
from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
|
||||
from .gpt import GPTIntegrationEndpoint
|
||||
|
||||
from .estimate import (
|
||||
ProjectEstimatePointEndpoint,
|
||||
BulkEstimatePointEndpoint,
|
||||
)
|
||||
|
||||
|
||||
from .release import ReleaseNotesEndpoint
|
||||
|
||||
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
|
||||
|
||||
from .analytic import (
|
||||
@@ -167,6 +169,4 @@ from .analytic import (
|
||||
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||
|
||||
from .exporter import ExportIssuesEndpoint
|
||||
|
||||
from .config import ConfigurationEndpoint
|
||||
from .exporter import ExportIssuesEndpoint
|
||||
@@ -1,40 +0,0 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
|
||||
|
||||
class ConfigurationEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
data = {}
|
||||
data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None)
|
||||
data["github"] = os.environ.get("GITHUB_CLIENT_ID", None)
|
||||
data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None)
|
||||
data["magic_login"] = (
|
||||
bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD)
|
||||
) and os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0") == "1"
|
||||
data["email_password_login"] = (
|
||||
os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1"
|
||||
)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -2,10 +2,9 @@
|
||||
import requests
|
||||
|
||||
# Third party imports
|
||||
import openai
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
import openai
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Django imports
|
||||
@@ -16,7 +15,6 @@ from .base import BaseAPIView
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Workspace, Project
|
||||
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
|
||||
from plane.utils.integrations.github import get_release_notes
|
||||
|
||||
|
||||
class GPTIntegrationEndpoint(BaseAPIView):
|
||||
@@ -75,44 +73,3 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class ReleaseNotesEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
try:
|
||||
release_notes = get_release_notes()
|
||||
return Response(release_notes, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class UnsplashEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
query = request.GET.get("query", False)
|
||||
page = request.GET.get("page", 1)
|
||||
per_page = request.GET.get("per_page", 20)
|
||||
|
||||
url = (
|
||||
f"https://api.unsplash.com/search/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}"
|
||||
if query
|
||||
else f"https://api.unsplash.com/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
resp = requests.get(url=url, headers=headers)
|
||||
return Response(resp.json(), status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -370,11 +370,6 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Check the issue status
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except InboxIssue.DoesNotExist:
|
||||
|
||||
@@ -24,6 +24,8 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.db import IntegrityError
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -56,6 +58,7 @@ from plane.api.serializers import (
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
ProjectEntityPermission,
|
||||
WorkSpaceAdminPermission,
|
||||
ProjectMemberPermission,
|
||||
@@ -83,6 +86,7 @@ from plane.db.models import (
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.grouper import group_results
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.export_task import issue_export_task
|
||||
|
||||
|
||||
class IssueViewSet(BaseViewSet):
|
||||
@@ -710,18 +714,10 @@ class LabelViewSet(BaseViewSet):
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
serializer = LabelSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IntegrityError:
|
||||
return Response({"error": "Label with the same name already exists in the project"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
@@ -2404,6 +2400,27 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
]
|
||||
serializer_class = IssueFlatSerializer
|
||||
model = Issue
|
||||
|
||||
|
||||
def perform_update(self, serializer):
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = (
|
||||
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||
)
|
||||
if current_instance is not None:
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch=int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
@@ -2597,47 +2614,6 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
serializer = IssueSerializer(
|
||||
issue, data=request.data, partial=True
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
if(request.data.get("is_draft") is not None and not request.data.get("is_draft")):
|
||||
serializer.save(created_at=timezone.now(), updated_at=timezone.now())
|
||||
else:
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.updated",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(issue).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp())
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Issue.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Issue does not exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
try:
|
||||
issue = Issue.objects.get(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Python imports
|
||||
import jwt
|
||||
import boto3
|
||||
from datetime import datetime
|
||||
|
||||
# Django imports
|
||||
@@ -496,7 +495,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
serializer_class = ProjectMemberAdminSerializer
|
||||
model = ProjectMember
|
||||
permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
@@ -618,8 +617,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except ProjectMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Project Member does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"error": "Project Member does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
@@ -1211,38 +1209,3 @@ class LeaveProjectEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
files = []
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
params = {
|
||||
"Bucket": settings.AWS_S3_BUCKET_NAME,
|
||||
"Prefix": "static/project-cover/",
|
||||
}
|
||||
|
||||
response = s3.list_objects_v2(**params)
|
||||
# Extracting file keys from the response
|
||||
if "Contents" in response:
|
||||
for content in response["Contents"]:
|
||||
if not content["Key"].endswith(
|
||||
"/"
|
||||
): # This line ensures we're only getting files, not "sub-folders"
|
||||
files.append(
|
||||
f"https://{settings.AWS_S3_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
|
||||
return Response(files, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response([], status=status.HTTP_200_OK)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.integrations.github import get_release_notes
|
||||
|
||||
|
||||
class ReleaseNotesEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
try:
|
||||
release_notes = get_release_notes()
|
||||
return Response(release_notes, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -49,16 +49,24 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
def filter_issues(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name", "sequence_id", "project__identifier"]
|
||||
q = Q()
|
||||
q_and = Q()
|
||||
for field in fields:
|
||||
if field == "sequence_id":
|
||||
sequences = re.findall(r"\d+\.\d+|\d+", query)
|
||||
for sequence_id in sequences:
|
||||
q |= Q(**{"sequence_id": sequence_id})
|
||||
if field in ["sequence_id", "project__identifier"]:
|
||||
# Condition to check sequence
|
||||
if field == "sequence_id":
|
||||
sequences = re.findall(r"\d+\.\d+|\d+", query)
|
||||
for sequence_id in sequences:
|
||||
q_and &= Q(**{"sequence_id": int(sequence_id)})
|
||||
# Condition to check identifier
|
||||
if field == "project__identifier":
|
||||
identifiers = query.split("-")
|
||||
if identifiers:
|
||||
q_and &= Q(**{"project__identifier": identifiers[0]})
|
||||
else:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
final_q = q_and | q
|
||||
issues = Issue.issue_objects.filter(
|
||||
q,
|
||||
final_q,
|
||||
project__project_projectmember__member=self.request.user,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ class GlobalViewViewSet(BaseViewSet):
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace")
|
||||
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@@ -243,6 +243,51 @@ class IssueViewViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
|
||||
class ViewIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id, view_id):
|
||||
try:
|
||||
view = IssueView.objects.get(pk=view_id)
|
||||
queries = view.query
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
issues = (
|
||||
Issue.issue_objects.filter(
|
||||
**queries, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
serializer = IssueLiteSerializer(issues, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except IssueView.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Issue View does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class IssueViewFavoriteViewSet(BaseViewSet):
|
||||
serializer_class = IssueViewFavoriteSerializer
|
||||
model = IssueViewFavorite
|
||||
|
||||
@@ -1197,7 +1197,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
projects = request.query_params.getlist("project", [])
|
||||
|
||||
queryset = IssueActivity.objects.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
~Q(field__in=["comment", "vote", "reaction"]),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
actor=user_id,
|
||||
|
||||
@@ -1389,7 +1389,7 @@ def issue_activity(
|
||||
):
|
||||
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
||||
|
||||
for subscriber in list(set(issue_subscribers)):
|
||||
for subscriber in issue_subscribers:
|
||||
for issue_activity in issue_activities_created:
|
||||
bulk_notifications.append(
|
||||
Notification(
|
||||
|
||||
@@ -33,8 +33,9 @@ def create_issue_relation(apps, schema_editor):
|
||||
def update_issue_priority_choice(apps, schema_editor):
|
||||
IssueModel = apps.get_model("db", "Issue")
|
||||
updated_issues = []
|
||||
for obj in IssueModel.objects.filter(priority=None):
|
||||
obj.priority = "none"
|
||||
for obj in IssueModel.objects.all():
|
||||
if obj.priority is None:
|
||||
obj.priority = "none"
|
||||
updated_issues.append(obj)
|
||||
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("db", "0044_auto_20230913_0709"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="GlobalView",
|
||||
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,),),
|
||||
("name", models.CharField(max_length=255, verbose_name="View Name")),
|
||||
("description", models.TextField(blank=True, verbose_name="View Description"),),
|
||||
("query", models.JSONField(verbose_name="View Query")),
|
||||
("access", models.PositiveSmallIntegerField(choices=[(0, "Private"), (1, "Public")], default=1),),
|
||||
("query_data", models.JSONField(default=dict)),
|
||||
("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",),),
|
||||
("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="global_views", to="db.workspace",),),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Global View",
|
||||
"verbose_name_plural": "Global Views",
|
||||
"db_table": "global_views",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issueactivity",
|
||||
name="epoch",
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
]
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-29 10:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import plane.db.models.workspace
|
||||
import uuid
|
||||
|
||||
|
||||
def update_issue_activity_priority(apps, schema_editor):
|
||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.filter(field="priority"):
|
||||
# Set the old and new value to none if it is empty for Priority
|
||||
obj.new_value = obj.new_value or "none"
|
||||
obj.old_value = obj.old_value or "none"
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(
|
||||
updated_issue_activity,
|
||||
["new_value", "old_value"],
|
||||
batch_size=2000,
|
||||
)
|
||||
|
||||
def update_issue_activity_blocked(apps, schema_editor):
|
||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.filter(field="blocks"):
|
||||
# Set the field to blocked_by
|
||||
obj.field = "blocked_by"
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(
|
||||
updated_issue_activity,
|
||||
["field"],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0044_auto_20230913_0709'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalView',
|
||||
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)),
|
||||
('name', models.CharField(max_length=255, verbose_name='View Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='View Description')),
|
||||
('query', models.JSONField(verbose_name='View Query')),
|
||||
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
|
||||
('query_data', models.JSONField(default=dict)),
|
||||
('sort_order', models.FloatField(default=65535)),
|
||||
('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')),
|
||||
('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='global_views', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global View',
|
||||
'verbose_name_plural': 'Global Views',
|
||||
'db_table': 'global_views',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspacemember',
|
||||
name='issue_props',
|
||||
field=models.JSONField(default=plane.db.models.workspace.get_issue_props),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueactivity',
|
||||
name='epoch',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
migrations.RunPython(update_issue_activity_priority),
|
||||
migrations.RunPython(update_issue_activity_blocked),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-26 10:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_issue_activity(apps, schema_editor):
|
||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.all():
|
||||
obj.epoch = int(obj.created_at.timestamp())
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(
|
||||
updated_issue_activity,
|
||||
["epoch"],
|
||||
batch_size=5000,
|
||||
)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0045_auto_20230915_0655'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_issue_activity),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-26 10:29
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_issue_activity_priority(apps, schema_editor):
|
||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.filter(field="priority"):
|
||||
# Set the old and new value to none if it is empty for Priority
|
||||
obj.new_value = obj.new_value or "none"
|
||||
obj.old_value = obj.old_value or "none"
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(
|
||||
updated_issue_activity,
|
||||
["new_value", "old_value"],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
def update_issue_activity_blocked(apps, schema_editor):
|
||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.filter(field="blocks"):
|
||||
# Set the field to blocked_by
|
||||
obj.field = "blocked_by"
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(
|
||||
updated_issue_activity,
|
||||
["field"],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0046_auto_20230926_1015'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_issue_activity_priority),
|
||||
migrations.RunPython(update_issue_activity_blocked),
|
||||
]
|
||||
@@ -17,23 +17,12 @@ class GlobalView(BaseModel):
|
||||
default=1, choices=((0, "Private"), (1, "Public"))
|
||||
)
|
||||
query_data = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Global View"
|
||||
verbose_name_plural = "Global Views"
|
||||
db_table = "global_views"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
largest_sort_order = GlobalView.objects.filter(
|
||||
workspace=self.workspace
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
|
||||
super(GlobalView, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the View"""
|
||||
|
||||
@@ -29,7 +29,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,
|
||||
@@ -54,15 +54,6 @@ def get_default_props():
|
||||
}
|
||||
|
||||
|
||||
def get_issue_props():
|
||||
return {
|
||||
"subscribed": True,
|
||||
"assigned": True,
|
||||
"created": True,
|
||||
"all_issues": True,
|
||||
}
|
||||
|
||||
|
||||
class Workspace(BaseModel):
|
||||
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
||||
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
||||
@@ -98,7 +89,6 @@ class WorkspaceMember(BaseModel):
|
||||
company_role = models.TextField(null=True, blank=True)
|
||||
view_props = models.JSONField(default=get_default_props)
|
||||
default_props = models.JSONField(default=get_default_props)
|
||||
issue_props = models.JSONField(default=get_issue_props)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "member"]
|
||||
|
||||
@@ -114,6 +114,3 @@ CELERY_BROKER_URL = os.environ.get("REDIS_URL")
|
||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||
|
||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||
|
||||
# Unsplash Access key
|
||||
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||
|
||||
@@ -7,7 +7,6 @@ import dj_database_url
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
@@ -90,112 +89,90 @@ if bool(os.environ.get("SENTRY_DSN", False)):
|
||||
profiles_sample_rate=1.0,
|
||||
)
|
||||
|
||||
if DOCKERIZED and USE_MINIO:
|
||||
INSTALLED_APPS += ("storages",)
|
||||
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
|
||||
# The name of the bucket to store files in.
|
||||
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get(
|
||||
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
|
||||
)
|
||||
# Default permissions
|
||||
AWS_DEFAULT_ACL = "public-read"
|
||||
AWS_QUERYSTRING_AUTH = False
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
# The AWS region to connect to.
|
||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||
|
||||
# Custom Domain settings
|
||||
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
|
||||
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
|
||||
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
|
||||
else:
|
||||
# The AWS region to connect to.
|
||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
||||
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
||||
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
||||
# The optional AWS session token to use.
|
||||
# AWS_SESSION_TOKEN = ""
|
||||
|
||||
# The optional AWS session token to use.
|
||||
# AWS_SESSION_TOKEN = ""
|
||||
# The name of the bucket to store files in.
|
||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
|
||||
# The name of the bucket to store files in.
|
||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
|
||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
||||
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
||||
# and their permissions will be set to "public-read".
|
||||
AWS_S3_BUCKET_AUTH = False
|
||||
|
||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
||||
# and their permissions will be set to "public-read".
|
||||
AWS_S3_BUCKET_AUTH = False
|
||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||
# is True. It also affects the "Cache-Control" header of the files.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||
|
||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||
# is True. It also affects the "Cache-Control" header of the files.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||
AWS_S3_PUBLIC_URL = ""
|
||||
|
||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||
AWS_S3_PUBLIC_URL = ""
|
||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||
# understand the consequences before enabling.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_REDUCED_REDUNDANCY = False
|
||||
|
||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||
# understand the consequences before enabling.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_REDUCED_REDUNDANCY = False
|
||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_DISPOSITION = ""
|
||||
|
||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_DISPOSITION = ""
|
||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_LANGUAGE = ""
|
||||
|
||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_LANGUAGE = ""
|
||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_METADATA = {}
|
||||
|
||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_METADATA = {}
|
||||
# If True, then files will be stored using AES256 server-side encryption.
|
||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||
# Otherwise, server-side encryption is not be enabled.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_ENCRYPT_KEY = False
|
||||
|
||||
# If True, then files will be stored using AES256 server-side encryption.
|
||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||
# Otherwise, server-side encryption is not be enabled.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_ENCRYPT_KEY = False
|
||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||
|
||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||
# compressed size is smaller than their uncompressed size.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_GZIP = True
|
||||
|
||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||
# compressed size is smaller than their uncompressed size.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_GZIP = True
|
||||
# The signature version to use for S3 requests.
|
||||
AWS_S3_SIGNATURE_VERSION = None
|
||||
|
||||
# The signature version to use for S3 requests.
|
||||
AWS_S3_SIGNATURE_VERSION = None
|
||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||
# extra characters appended.
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
|
||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||
# extra characters appended.
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
STORAGES["default"] = {
|
||||
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||
}
|
||||
|
||||
STORAGES["default"] = {
|
||||
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||
}
|
||||
# AWS Settings End
|
||||
|
||||
# Enable Connection Pooling (if desired)
|
||||
@@ -216,27 +193,16 @@ CSRF_COOKIE_SECURE = True
|
||||
|
||||
REDIS_URL = os.environ.get("REDIS_URL")
|
||||
|
||||
if DOCKERIZED:
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
}
|
||||
}
|
||||
else:
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||
},
|
||||
}
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
|
||||
@@ -259,12 +225,8 @@ broker_url = (
|
||||
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||
)
|
||||
|
||||
if DOCKERIZED:
|
||||
CELERY_BROKER_URL = REDIS_URL
|
||||
CELERY_RESULT_BACKEND = REDIS_URL
|
||||
else:
|
||||
CELERY_BROKER_URL = broker_url
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
CELERY_BROKER_URL = broker_url
|
||||
|
||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||
|
||||
@@ -276,6 +238,3 @@ SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
||||
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
||||
SCOUT_NAME = "Plane"
|
||||
|
||||
# Unsplash Access key
|
||||
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||
|
||||
|
||||
@@ -126,4 +126,3 @@ ANALYTICS_BASE_API = False
|
||||
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
||||
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
|
||||
|
||||
|
||||
@@ -218,7 +218,3 @@ CELERY_BROKER_URL = broker_url
|
||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||
|
||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||
|
||||
|
||||
# Unsplash Access key
|
||||
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||
|
||||
@@ -1,62 +1,5 @@
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
# The date from pattern
|
||||
pattern = re.compile(r"\d+_(weeks|months)$")
|
||||
|
||||
|
||||
# Get the 2_weeks, 3_months
|
||||
def string_date_filter(filter, duration, subsequent, term, date_filter, offset):
|
||||
now = timezone.now().date()
|
||||
if term == "months":
|
||||
if subsequent == "after":
|
||||
if offset == "fromnow":
|
||||
filter[f"{date_filter}__gte"] = now + timedelta(days=duration * 30)
|
||||
else:
|
||||
filter[f"{date_filter}__gte"] = now - timedelta(days=duration * 30)
|
||||
else:
|
||||
if offset == "fromnow":
|
||||
filter[f"{date_filter}__lte"] = now + timedelta(days=duration * 30)
|
||||
else:
|
||||
filter[f"{date_filter}__lte"] = now - timedelta(days=duration * 30)
|
||||
if term == "weeks":
|
||||
if subsequent == "after":
|
||||
if offset == "fromnow":
|
||||
filter[f"{date_filter}__gte"] = now + timedelta(weeks=duration)
|
||||
else:
|
||||
filter[f"{date_filter}__gte"] = now - timedelta(weeks=duration)
|
||||
else:
|
||||
if offset == "fromnow":
|
||||
filter[f"{date_filter}__lte"] = now + timedelta(days=duration)
|
||||
else:
|
||||
filter[f"{date_filter}__lte"] = now - timedelta(days=duration)
|
||||
|
||||
|
||||
def date_filter(filter, date_term, queries):
|
||||
"""
|
||||
Handle all date filters
|
||||
"""
|
||||
for query in queries:
|
||||
date_query = query.split(";")
|
||||
if len(date_query) >= 2:
|
||||
match = pattern.match(date_query[0])
|
||||
if match:
|
||||
if len(date_query) == 3:
|
||||
digit, term = date_query[0].split("_")
|
||||
string_date_filter(
|
||||
filter=filter,
|
||||
duration=int(digit),
|
||||
subsequent=date_query[1],
|
||||
term=term,
|
||||
date_filter="created_at__date",
|
||||
offset=date_query[2],
|
||||
)
|
||||
else:
|
||||
if "after" in date_query:
|
||||
filter[f"{date_term}__gte"] = date_query[0]
|
||||
else:
|
||||
filter[f"{date_term}__lte"] = date_query[0]
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
|
||||
def filter_state(params, filter, method):
|
||||
@@ -154,10 +97,20 @@ def filter_created_at(params, filter, method):
|
||||
if method == "GET":
|
||||
created_ats = params.get("created_at").split(",")
|
||||
if len(created_ats) and "" not in created_ats:
|
||||
date_filter(filter=filter, date_term="created_at__date", queries=created_ats)
|
||||
for query in created_ats:
|
||||
created_at_query = query.split(";")
|
||||
if len(created_at_query) == 2 and "after" in created_at_query:
|
||||
filter["created_at__date__gte"] = created_at_query[0]
|
||||
else:
|
||||
filter["created_at__date__lte"] = created_at_query[0]
|
||||
else:
|
||||
if params.get("created_at", None) and len(params.get("created_at")):
|
||||
date_filter(filter=filter, date_term="created_at__date", queries=params.get("created_at", []))
|
||||
for query in params.get("created_at"):
|
||||
created_at_query = query.split(";")
|
||||
if len(created_at_query) == 2 and "after" in created_at_query:
|
||||
filter["created_at__date__gte"] = created_at_query[0]
|
||||
else:
|
||||
filter["created_at__date__lte"] = created_at_query[0]
|
||||
return filter
|
||||
|
||||
|
||||
@@ -165,10 +118,20 @@ def filter_updated_at(params, filter, method):
|
||||
if method == "GET":
|
||||
updated_ats = params.get("updated_at").split(",")
|
||||
if len(updated_ats) and "" not in updated_ats:
|
||||
date_filter(filter=filter, date_term="created_at__date", queries=updated_ats)
|
||||
for query in updated_ats:
|
||||
updated_at_query = query.split(";")
|
||||
if len(updated_at_query) == 2 and "after" in updated_at_query:
|
||||
filter["updated_at__date__gte"] = updated_at_query[0]
|
||||
else:
|
||||
filter["updated_at__date__lte"] = updated_at_query[0]
|
||||
else:
|
||||
if params.get("updated_at", None) and len(params.get("updated_at")):
|
||||
date_filter(filter=filter, date_term="created_at__date", queries=params.get("updated_at", []))
|
||||
for query in params.get("updated_at"):
|
||||
updated_at_query = query.split(";")
|
||||
if len(updated_at_query) == 2 and "after" in updated_at_query:
|
||||
filter["updated_at__date__gte"] = updated_at_query[0]
|
||||
else:
|
||||
filter["updated_at__date__lte"] = updated_at_query[0]
|
||||
return filter
|
||||
|
||||
|
||||
@@ -176,10 +139,20 @@ def filter_start_date(params, filter, method):
|
||||
if method == "GET":
|
||||
start_dates = params.get("start_date").split(",")
|
||||
if len(start_dates) and "" not in start_dates:
|
||||
date_filter(filter=filter, date_term="start_date", queries=start_dates)
|
||||
for query in start_dates:
|
||||
start_date_query = query.split(";")
|
||||
if len(start_date_query) == 2 and "after" in start_date_query:
|
||||
filter["start_date__gte"] = start_date_query[0]
|
||||
else:
|
||||
filter["start_date__lte"] = start_date_query[0]
|
||||
else:
|
||||
if params.get("start_date", None) and len(params.get("start_date")):
|
||||
date_filter(filter=filter, date_term="start_date", queries=params.get("start_date", []))
|
||||
for query in params.get("start_date"):
|
||||
start_date_query = query.split(";")
|
||||
if len(start_date_query) == 2 and "after" in start_date_query:
|
||||
filter["start_date__gte"] = start_date_query[0]
|
||||
else:
|
||||
filter["start_date__lte"] = start_date_query[0]
|
||||
return filter
|
||||
|
||||
|
||||
@@ -187,11 +160,21 @@ def filter_target_date(params, filter, method):
|
||||
if method == "GET":
|
||||
target_dates = params.get("target_date").split(",")
|
||||
if len(target_dates) and "" not in target_dates:
|
||||
|
||||
date_filter(filter=filter, date_term="target_date", queries=target_dates)
|
||||
for query in target_dates:
|
||||
target_date_query = query.split(";")
|
||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||
filter["target_date__gt"] = target_date_query[0]
|
||||
else:
|
||||
filter["target_date__lt"] = target_date_query[0]
|
||||
else:
|
||||
if params.get("target_date", None) and len(params.get("target_date")):
|
||||
date_filter(filter=filter, date_term="target_date", queries=params.get("target_date", []))
|
||||
for query in params.get("target_date"):
|
||||
target_date_query = query.split(";")
|
||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||
filter["target_date__gt"] = target_date_query[0]
|
||||
else:
|
||||
filter["target_date__lt"] = target_date_query[0]
|
||||
|
||||
return filter
|
||||
|
||||
|
||||
@@ -199,10 +182,20 @@ def filter_completed_at(params, filter, method):
|
||||
if method == "GET":
|
||||
completed_ats = params.get("completed_at").split(",")
|
||||
if len(completed_ats) and "" not in completed_ats:
|
||||
date_filter(filter=filter, date_term="completed_at__date", queries=completed_ats)
|
||||
for query in completed_ats:
|
||||
completed_at_query = query.split(";")
|
||||
if len(completed_at_query) == 2 and "after" in completed_at_query:
|
||||
filter["completed_at__date__gte"] = completed_at_query[0]
|
||||
else:
|
||||
filter["completed_at__lte"] = completed_at_query[0]
|
||||
else:
|
||||
if params.get("completed_at", None) and len(params.get("completed_at")):
|
||||
date_filter(filter=filter, date_term="completed_at__date", queries=params.get("completed_at", []))
|
||||
for query in params.get("completed_at"):
|
||||
completed_at_query = query.split(";")
|
||||
if len(completed_at_query) == 2 and "after" in completed_at_query:
|
||||
filter["completed_at__date__gte"] = completed_at_query[0]
|
||||
else:
|
||||
filter["completed_at__lte"] = completed_at_query[0]
|
||||
return filter
|
||||
|
||||
|
||||
|
||||
@@ -9,15 +9,27 @@ from plane.db.models import Issue
|
||||
|
||||
|
||||
def search_issues(query, queryset):
|
||||
fields = ["name", "sequence_id"]
|
||||
fields = ["name", "sequence_id", "project__identifier"]
|
||||
q = Q()
|
||||
q_and = Q() # Q object for AND conditions
|
||||
|
||||
for field in fields:
|
||||
if field == "sequence_id":
|
||||
sequences = re.findall(r"\d+\.\d+|\d+", query)
|
||||
for sequence_id in sequences:
|
||||
q |= Q(**{"sequence_id": sequence_id})
|
||||
if field in ["sequence_id", "project__identifier"]:
|
||||
# Condition to check sequence
|
||||
if field == "sequence_id":
|
||||
sequences = re.findall(r"\d+\.\d+|\d+", query)
|
||||
for sequence_id in sequences:
|
||||
q_and &= Q(**{"sequence_id": int(sequence_id)})
|
||||
# Condition to check identifier
|
||||
if field == "project__identifier":
|
||||
identifiers = query.split("-")
|
||||
if identifiers:
|
||||
q_and &= Q(**{"project__identifier": identifiers[0]})
|
||||
else:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
final_q = q_and | q
|
||||
|
||||
return queryset.filter(
|
||||
q,
|
||||
final_q,
|
||||
).distinct()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.13.2",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"version": "0.13.2",
|
||||
"version": "0.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.13.2",
|
||||
"version": "0.0.1",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.13.2",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# UI Package
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "0.13.2",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.tsx",
|
||||
"types": "./index.tsx",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -10,12 +10,9 @@ import githubWhiteImage from "public/logos/github-white.svg";
|
||||
|
||||
export interface GithubLoginButtonProps {
|
||||
handleSignIn: React.Dispatch<string>;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
const { handleSignIn, clientId } = props;
|
||||
// states
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn }) => {
|
||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
||||
const [gitCode, setGitCode] = useState<null | string>(null);
|
||||
|
||||
@@ -41,7 +38,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<Link
|
||||
className="w-full"
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||
>
|
||||
<button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]">
|
||||
<Image
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { FC, useEffect, useRef, useCallback, useState } from "react";
|
||||
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react";
|
||||
|
||||
import Script from "next/script";
|
||||
|
||||
export interface IGoogleLoginButton {
|
||||
clientId: string;
|
||||
text?: string;
|
||||
handleSignIn: React.Dispatch<any>;
|
||||
styles?: CSSProperties;
|
||||
}
|
||||
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
const { handleSignIn, clientId } = props;
|
||||
// refs
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => {
|
||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||
// states
|
||||
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
|
||||
|
||||
const loadScript = useCallback(() => {
|
||||
if (!googleSignInButton.current || gsiScriptLoaded) return;
|
||||
|
||||
(window as any)?.google?.accounts.id.initialize({
|
||||
client_id: clientId,
|
||||
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
|
||||
callback: handleSignIn,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import React from "react";
|
||||
import useSWR from "swr";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import authenticationService from "services/authentication.service";
|
||||
import { AppConfigService } from "services/app-config.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmailPasswordForm, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
||||
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
||||
// images
|
||||
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||
|
||||
const appConfig = new AppConfigService();
|
||||
|
||||
export const SignInView = observer(() => {
|
||||
const { user: userStore } = useMobxStore();
|
||||
// router
|
||||
|
||||
const router = useRouter();
|
||||
const { next_path } = router.query as { next_path: string };
|
||||
// toast
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
// fetch app config
|
||||
const { data } = useSWR("APP_CONFIG", () => appConfig.envConfig());
|
||||
|
||||
const onSignInError = (error: any) => {
|
||||
setToastAlert({
|
||||
@@ -35,17 +31,17 @@ export const SignInView = observer(() => {
|
||||
};
|
||||
|
||||
const onSignInSuccess = (response: any) => {
|
||||
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
||||
|
||||
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/login";
|
||||
|
||||
userStore.setCurrentUser(response?.user);
|
||||
|
||||
const isOnboard = response?.user?.onboarding_step?.profile_complete || false;
|
||||
|
||||
if (isOnboard) {
|
||||
if (next_path) router.push(next_path);
|
||||
else router.push("/login");
|
||||
} else {
|
||||
if (next_path) router.push(`/onboarding?next_path=${next_path}`);
|
||||
else router.push("/onboarding");
|
||||
if (!isOnboarded) {
|
||||
router.push(`/onboarding?next_path=${nextPath}`);
|
||||
return;
|
||||
}
|
||||
router.push((nextPath ?? "/login").toString());
|
||||
};
|
||||
|
||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||
@@ -67,6 +63,24 @@ export const SignInView = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleGitHubSignIn = async (credential: string) => {
|
||||
try {
|
||||
if (process.env.NEXT_PUBLIC_GITHUB_ID && credential) {
|
||||
const socialAuthPayload = {
|
||||
medium: "github",
|
||||
credential,
|
||||
clientId: process.env.NEXT_PUBLIC_GITHUB_ID,
|
||||
};
|
||||
const response = await authenticationService.socialAuth(socialAuthPayload);
|
||||
onSignInSuccess(response);
|
||||
} else {
|
||||
throw Error("Cant find credentials");
|
||||
}
|
||||
} catch (err: any) {
|
||||
onSignInError(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordSignIn = async (formData: any) => {
|
||||
await authenticationService
|
||||
.emailLogin(formData)
|
||||
@@ -104,32 +118,38 @@ export const SignInView = observer(() => {
|
||||
</div>
|
||||
<div className="grid place-items-center h-full overflow-y-auto py-5 px-7">
|
||||
<div>
|
||||
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">Sign in to Plane</h1>
|
||||
{data?.email_password_login && <EmailPasswordForm onSubmit={handlePasswordSignIn} />}
|
||||
|
||||
{data?.magic_login && (
|
||||
<div className="flex flex-col divide-y divide-custom-border-200">
|
||||
<div className="pb-7">
|
||||
<EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
|
||||
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
|
||||
<>
|
||||
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
|
||||
Sign in to Plane
|
||||
</h1>
|
||||
<div className="flex flex-col divide-y divide-custom-border-200">
|
||||
<div className="pb-7">
|
||||
<EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
|
||||
<GoogleLoginButton handleSignIn={handleGoogleSignIn} />
|
||||
{/* <GithubLoginButton handleSignIn={handleGitHubSignIn} /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmailPasswordForm onSubmit={handlePasswordSignIn} />
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
|
||||
{data?.google && <GoogleLoginButton clientId={data.google} handleSignIn={handleGoogleSignIn} />}
|
||||
</div>
|
||||
|
||||
<p className="pt-16 text-custom-text-200 text-sm text-center">
|
||||
By signing up, you agree to the{" "}
|
||||
<a
|
||||
href="https://plane.so/terms-and-conditions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium underline"
|
||||
>
|
||||
Terms & Conditions
|
||||
</a>
|
||||
</p>
|
||||
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
|
||||
<p className="pt-16 text-custom-text-200 text-sm text-center">
|
||||
By signing up, you agree to the{" "}
|
||||
<a
|
||||
href="https://plane.so/terms-and-conditions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium underline"
|
||||
>
|
||||
Terms & Conditions
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,43 +44,19 @@ const IssueNavbar = observer(() => {
|
||||
}, [projectStore, workspace_slug, project_slug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace_slug && project_slug && projectStore?.deploySettings) {
|
||||
const viewsAcceptable: string[] = [];
|
||||
let currentBoard: string | null = null;
|
||||
|
||||
if (projectStore?.deploySettings?.views?.list) viewsAcceptable.push("list");
|
||||
if (projectStore?.deploySettings?.views?.kanban) viewsAcceptable.push("kanban");
|
||||
if (projectStore?.deploySettings?.views?.calendar) viewsAcceptable.push("calendar");
|
||||
if (projectStore?.deploySettings?.views?.gantt) viewsAcceptable.push("gantt");
|
||||
if (projectStore?.deploySettings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet");
|
||||
|
||||
if (board) {
|
||||
if (viewsAcceptable.includes(board.toString())) {
|
||||
currentBoard = board.toString();
|
||||
} else {
|
||||
if (viewsAcceptable && viewsAcceptable.length > 0) {
|
||||
currentBoard = viewsAcceptable[0];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (viewsAcceptable && viewsAcceptable.length > 0) {
|
||||
currentBoard = viewsAcceptable[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBoard) {
|
||||
if (projectStore?.activeBoard === null || projectStore?.activeBoard !== currentBoard) {
|
||||
projectStore.setActiveBoard(currentBoard);
|
||||
router.push({
|
||||
pathname: `/${workspace_slug}/${project_slug}`,
|
||||
query: {
|
||||
board: currentBoard,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (workspace_slug && project_slug) {
|
||||
if (!board) {
|
||||
router.push({
|
||||
pathname: `/${workspace_slug}/${project_slug}`,
|
||||
query: {
|
||||
board: "list",
|
||||
},
|
||||
});
|
||||
return projectStore.setActiveBoard("list");
|
||||
}
|
||||
projectStore.setActiveBoard(board.toString());
|
||||
}
|
||||
}, [board, workspace_slug, project_slug, router, projectStore, projectStore?.deploySettings]);
|
||||
}, [board, workspace_slug, project_slug]);
|
||||
|
||||
return (
|
||||
<div className="px-5 relative w-full flex items-center gap-4">
|
||||
@@ -129,7 +105,7 @@ const IssueNavbar = observer(() => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-shrink-0">
|
||||
<Link href={`/login/?next_path=${router.asPath}`}>
|
||||
<Link href={`/?next_path=${router.asPath}`}>
|
||||
<a>
|
||||
<PrimaryButton className="flex-shrink-0" outline>
|
||||
Sign in
|
||||
|
||||
@@ -7,13 +7,7 @@ import { SignInView, UserLoggedIn } from "components/accounts";
|
||||
export const LoginView = observer(() => {
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{userStore?.loader ? (
|
||||
<div className="relative w-screen h-screen flex justify-center items-center">Loading</div>
|
||||
) : (
|
||||
<>{userStore.currentUser ? <UserLoggedIn /> : <SignInView />}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (!userStore.currentUser) return <SignInView />;
|
||||
|
||||
return <UserLoggedIn />;
|
||||
});
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
import { useEffect } from "react";
|
||||
// next imports
|
||||
import { useRouter } from "next/router";
|
||||
// js cookie
|
||||
import Cookie from "js-cookie";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
const MobxStoreInit = () => {
|
||||
const { user: userStore }: RootStore = useMobxStore();
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { states, labels, priorities } = router.query as { states: string[]; labels: string[]; priorities: string[] };
|
||||
@@ -21,11 +19,6 @@ const MobxStoreInit = () => {
|
||||
// store.issue.userSelectedStates = states || [];
|
||||
// }, [store.issue]);
|
||||
|
||||
useEffect(() => {
|
||||
const authToken = Cookie.get("accessToken") || null;
|
||||
if (authToken) userStore.fetchCurrentUser();
|
||||
}, [userStore]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.13.2",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 4000",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
// next
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const Index: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { next_path } = router.query as { next_path: string };
|
||||
|
||||
useEffect(() => {
|
||||
if (next_path) router.push(`/login?next_path=${next_path}`);
|
||||
else router.push(`/login`);
|
||||
}, [router, next_path]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -5,4 +5,4 @@ import { LoginView } from "components/views";
|
||||
|
||||
const LoginPage = () => <LoginView />;
|
||||
|
||||
export default LoginPage;
|
||||
export default LoginPage;
|
||||
@@ -1,30 +0,0 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
// helper
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export interface IEnvConfig {
|
||||
github: string;
|
||||
google: string;
|
||||
github_app_name: string | null;
|
||||
email_password_login: boolean;
|
||||
magic_login: boolean;
|
||||
}
|
||||
|
||||
export class AppConfigService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async envConfig(): Promise<IEnvConfig> {
|
||||
return this.get("/api/configs/", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,24 @@ class FileServices extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUnsplashImages(page: number = 1, query?: string): Promise<UnSplashImage[]> {
|
||||
const url = "/api/unsplash";
|
||||
|
||||
return this.request({
|
||||
method: "get",
|
||||
url,
|
||||
params: {
|
||||
page,
|
||||
per_page: 20,
|
||||
query,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data?.results ?? response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const fileServices = new FileServices();
|
||||
|
||||
@@ -7,17 +7,12 @@ import { ActorDetail } from "types/issue";
|
||||
import { IUser } from "types/user";
|
||||
|
||||
export interface IUserStore {
|
||||
loader: boolean;
|
||||
error: any | null;
|
||||
currentUser: any | null;
|
||||
fetchCurrentUser: () => void;
|
||||
currentActor: () => any;
|
||||
}
|
||||
|
||||
class UserStore implements IUserStore {
|
||||
loader: boolean = false;
|
||||
error: any | null = null;
|
||||
|
||||
currentUser: IUser | null = null;
|
||||
// root store
|
||||
rootStore;
|
||||
@@ -27,9 +22,6 @@ class UserStore implements IUserStore {
|
||||
constructor(_rootStore: any) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
loader: observable.ref,
|
||||
error: observable.ref,
|
||||
|
||||
currentUser: observable.ref,
|
||||
// actions
|
||||
setCurrentUser: action,
|
||||
@@ -81,19 +73,14 @@ class UserStore implements IUserStore {
|
||||
|
||||
fetchCurrentUser = async () => {
|
||||
try {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
const response = await this.userService.currentUser();
|
||||
if (response) {
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.currentUser = response;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch current user", error);
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalEnv": [
|
||||
"NEXT_PUBLIC_GITHUB_ID",
|
||||
"NEXT_PUBLIC_GOOGLE_CLIENTID",
|
||||
"NEXT_PUBLIC_API_BASE_URL",
|
||||
"NEXT_PUBLIC_DEPLOY_URL",
|
||||
"API_BASE_URL",
|
||||
@@ -10,6 +12,8 @@
|
||||
"NEXT_PUBLIC_GITHUB_APP_NAME",
|
||||
"NEXT_PUBLIC_ENABLE_SENTRY",
|
||||
"NEXT_PUBLIC_ENABLE_OAUTH",
|
||||
"NEXT_PUBLIC_UNSPLASH_ACCESS",
|
||||
"NEXT_PUBLIC_UNSPLASH_ENABLED",
|
||||
"NEXT_PUBLIC_TRACK_EVENTS",
|
||||
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
||||
"NEXT_PUBLIC_CRISP_ID",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
// react hook form
|
||||
import { useForm } from "react-hook-form";
|
||||
// components
|
||||
import { EmailResetPasswordForm } from "components/account";
|
||||
// ui
|
||||
import { Input, PrimaryButton } from "components/ui";
|
||||
// types
|
||||
@@ -11,12 +18,14 @@ type EmailPasswordFormValues = {
|
||||
|
||||
type Props = {
|
||||
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
|
||||
setIsResettingPassword: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const EmailPasswordForm: React.FC<Props> = (props) => {
|
||||
const { onSubmit, setIsResettingPassword } = props;
|
||||
// form info
|
||||
export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
|
||||
const [isResettingPassword, setIsResettingPassword] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const isSignUpPage = router.pathname === "/sign-up";
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -33,62 +42,94 @@ export const EmailPasswordForm: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Email address is required",
|
||||
validate: (value) =>
|
||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
value
|
||||
) || "Email address is not valid",
|
||||
}}
|
||||
error={errors.email}
|
||||
placeholder="Enter your email address..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Password is required",
|
||||
}}
|
||||
error={errors.password}
|
||||
placeholder="Enter your password..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsResettingPassword(true)}
|
||||
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||
>
|
||||
Forgot your password?
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="w-full text-center h-[46px]"
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
|
||||
{isResettingPassword
|
||||
? "Reset your password"
|
||||
: isSignUpPage
|
||||
? "Sign up on Plane"
|
||||
: "Sign in to Plane"}
|
||||
</h1>
|
||||
{isResettingPassword ? (
|
||||
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
|
||||
) : (
|
||||
<form
|
||||
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Email address is required",
|
||||
validate: (value) =>
|
||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
value
|
||||
) || "Email address is not valid",
|
||||
}}
|
||||
error={errors.email}
|
||||
placeholder="Enter your email address..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Password is required",
|
||||
}}
|
||||
error={errors.password}
|
||||
placeholder="Enter your password..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right text-xs">
|
||||
{isSignUpPage ? (
|
||||
<Link href="/">
|
||||
<a className="text-custom-text-200 hover:text-custom-primary-100">
|
||||
Already have an account? Sign in.
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsResettingPassword(true)}
|
||||
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||
>
|
||||
Forgot your password?
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="w-full text-center h-[46px]"
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSignUpPage
|
||||
? isSubmitting
|
||||
? "Signing up..."
|
||||
: "Sign up"
|
||||
: isSubmitting
|
||||
? "Signing in..."
|
||||
: "Sign in"}
|
||||
</PrimaryButton>
|
||||
{!isSignUpPage && (
|
||||
<Link href="/sign-up">
|
||||
<a className="block text-custom-text-200 hover:text-custom-primary-100 text-xs mt-4">
|
||||
Don{"'"}t have an account? Sign up.
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { useForm } from "react-hook-form";
|
||||
// ui
|
||||
import { Input, PrimaryButton } from "components/ui";
|
||||
// types
|
||||
type EmailPasswordFormValues = {
|
||||
email: string;
|
||||
password?: string;
|
||||
confirm_password: string;
|
||||
medium?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
|
||||
};
|
||||
|
||||
export const EmailSignUpForm: React.FC<Props> = (props) => {
|
||||
const { onSubmit } = props;
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<EmailPasswordFormValues>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
confirm_password: "",
|
||||
medium: "email",
|
||||
},
|
||||
mode: "onChange",
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Email address is required",
|
||||
validate: (value) =>
|
||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
value
|
||||
) || "Email address is not valid",
|
||||
}}
|
||||
error={errors.email}
|
||||
placeholder="Enter your email address..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Password is required",
|
||||
}}
|
||||
error={errors.password}
|
||||
placeholder="Enter your password..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="confirm_password"
|
||||
type="password"
|
||||
name="confirm_password"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Password is required",
|
||||
validate: (val: string) => {
|
||||
if (watch("password") != val) {
|
||||
return "Your passwords do no match";
|
||||
}
|
||||
},
|
||||
}}
|
||||
error={errors.confirm_password}
|
||||
placeholder="Confirm your password..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right text-xs">
|
||||
<Link href="/">
|
||||
<a className="text-custom-text-200 hover:text-custom-primary-100">
|
||||
Already have an account? Sign in.
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="w-full text-center h-[46px]"
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Signing up..." : "Sign up"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +1,29 @@
|
||||
import { useEffect, useState, FC } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// images
|
||||
import githubBlackImage from "/public/logos/github-black.png";
|
||||
import githubWhiteImage from "/public/logos/github-white.png";
|
||||
|
||||
const { NEXT_PUBLIC_GITHUB_ID } = process.env;
|
||||
|
||||
export interface GithubLoginButtonProps {
|
||||
handleSignIn: React.Dispatch<string>;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
const { handleSignIn, clientId } = props;
|
||||
// states
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn }) => {
|
||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
||||
const [gitCode, setGitCode] = useState<null | string>(null);
|
||||
// router
|
||||
|
||||
const {
|
||||
query: { code },
|
||||
} = useRouter();
|
||||
// theme
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -40,7 +42,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
return (
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<Link
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||
>
|
||||
<button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]">
|
||||
<Image
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { FC, useEffect, useRef, useCallback, useState } from "react";
|
||||
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react";
|
||||
|
||||
import Script from "next/script";
|
||||
|
||||
export interface IGoogleLoginButton {
|
||||
text?: string;
|
||||
handleSignIn: React.Dispatch<any>;
|
||||
clientId: string;
|
||||
styles?: CSSProperties;
|
||||
}
|
||||
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
const { handleSignIn, clientId } = props;
|
||||
// refs
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => {
|
||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||
// states
|
||||
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
|
||||
|
||||
const loadScript = useCallback(() => {
|
||||
if (!googleSignInButton.current || gsiScriptLoaded) return;
|
||||
|
||||
window?.google?.accounts.id.initialize({
|
||||
client_id: clientId,
|
||||
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
|
||||
callback: handleSignIn,
|
||||
});
|
||||
|
||||
@@ -40,7 +39,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
|
||||
|
||||
setGsiScriptLoaded(true);
|
||||
}, [handleSignIn, gsiScriptLoaded, clientId]);
|
||||
}, [handleSignIn, gsiScriptLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (window?.google?.accounts?.id) {
|
||||
|
||||
@@ -3,4 +3,3 @@ export * from "./email-password-form";
|
||||
export * from "./email-reset-password-form";
|
||||
export * from "./github-login-button";
|
||||
export * from "./google-login";
|
||||
export * from "./email-signup-form";
|
||||
|
||||
@@ -13,14 +13,9 @@ import { IProject } from "types";
|
||||
type Props = {
|
||||
projectDetails: IProject | undefined;
|
||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const AutoArchiveAutomation: React.FC<Props> = ({
|
||||
projectDetails,
|
||||
handleChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
|
||||
const [monthModal, setmonthModal] = useState(false);
|
||||
|
||||
const initialValues: Partial<IProject> = { archive_in: 1 };
|
||||
@@ -54,7 +49,6 @@ export const AutoArchiveAutomation: React.FC<Props> = ({
|
||||
: handleChange({ archive_in: 0 })
|
||||
}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -74,8 +68,8 @@ export const AutoArchiveAutomation: React.FC<Props> = ({
|
||||
handleChange({ archive_in: val });
|
||||
}}
|
||||
input
|
||||
verticalPosition="bottom"
|
||||
width="w-full"
|
||||
disabled={disabled}
|
||||
>
|
||||
<>
|
||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||
|
||||
@@ -24,14 +24,9 @@ import { getStatesList } from "helpers/state.helper";
|
||||
type Props = {
|
||||
projectDetails: IProject | undefined;
|
||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const AutoCloseAutomation: React.FC<Props> = ({
|
||||
projectDetails,
|
||||
handleChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
|
||||
const [monthModal, setmonthModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
@@ -103,7 +98,6 @@ export const AutoCloseAutomation: React.FC<Props> = ({
|
||||
: handleChange({ close_in: 0, default_state: null })
|
||||
}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +119,6 @@ export const AutoCloseAutomation: React.FC<Props> = ({
|
||||
}}
|
||||
input
|
||||
width="w-full"
|
||||
disabled={disabled}
|
||||
>
|
||||
<>
|
||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||
|
||||
@@ -161,6 +161,7 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
/>
|
||||
<CreateUpdateViewModal
|
||||
handleClose={() => setIsCreateViewModalOpen(false)}
|
||||
viewType="project"
|
||||
isOpen={isCreateViewModalOpen}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
@@ -2,33 +2,13 @@ import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// hook
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// icons
|
||||
import { Icon, Tooltip } from "components/ui";
|
||||
import {
|
||||
TagIcon,
|
||||
CopyPlus,
|
||||
Calendar,
|
||||
Link2Icon,
|
||||
RocketIcon,
|
||||
Users2Icon,
|
||||
ArchiveIcon,
|
||||
PaperclipIcon,
|
||||
ContrastIcon,
|
||||
TriangleIcon,
|
||||
LayoutGridIcon,
|
||||
SignalMediumIcon,
|
||||
MessageSquareIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
BlockedIcon,
|
||||
BlockerIcon,
|
||||
RelatedIcon,
|
||||
StackedLayersHorizontalIcon,
|
||||
} from "components/icons";
|
||||
import { CopyPlus } from "lucide-react";
|
||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
@@ -56,7 +36,7 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
{activity.issue_detail
|
||||
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
|
||||
: "Issue"}
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -97,18 +77,6 @@ const LabelPill = ({ labelId }: { labelId: string }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
const EstimatePoint = ({ point }: { point: string }) => {
|
||||
const { estimateValue, isEstimateActive } = useEstimateOption(Number(point));
|
||||
const currentPoint = Number(point) + 1;
|
||||
|
||||
return (
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{isEstimateActive
|
||||
? estimateValue
|
||||
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
@@ -149,14 +117,14 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Users2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
archived_at: {
|
||||
message: (activity) => {
|
||||
if (activity.new_value === "restore") return "restored the issue.";
|
||||
else return "archived the issue.";
|
||||
},
|
||||
icon: <ArchiveIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
attachment: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -171,7 +139,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
attachment
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
@@ -195,7 +163,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <PaperclipIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
blocking: {
|
||||
message: (activity) => {
|
||||
@@ -286,7 +254,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
@@ -301,7 +269,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
@@ -316,12 +284,12 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.old_value}
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: (activity, showIssue) => (
|
||||
@@ -336,7 +304,7 @@ const activityDetails: {
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -356,7 +324,8 @@ const activityDetails: {
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the estimate point to <EstimatePoint point={activity.new_value} />
|
||||
set the estimate point to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
@@ -367,14 +336,14 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <TriangleIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
issue: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created") return "created the issue.";
|
||||
else return "deleted an issue.";
|
||||
},
|
||||
icon: <StackedLayersHorizontalIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -411,7 +380,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <TagIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
link: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -426,7 +395,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
@@ -448,7 +417,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
@@ -470,7 +439,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
@@ -482,7 +451,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Link2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
@@ -497,7 +466,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
@@ -512,7 +481,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
@@ -527,12 +496,12 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.old_value}
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="dataset" className="!text-xs !text-[#6b7280]" aria-hidden="true" />,
|
||||
icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: (activity, showIssue) => (
|
||||
@@ -547,7 +516,7 @@ const activityDetails: {
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -580,13 +549,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: (
|
||||
<Icon
|
||||
iconName="supervised_user_circle"
|
||||
className="!text-xs !text-[#6b7280]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
priority: {
|
||||
message: (activity, showIssue) => (
|
||||
@@ -604,7 +567,7 @@ const activityDetails: {
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
start_date: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -638,7 +601,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: (activity, showIssue) => (
|
||||
@@ -654,7 +617,7 @@ const activityDetails: {
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <LayoutGridIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Squares2X2Icon className="h-6 w-6 text-custom-sidebar-200" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -688,7 +651,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,14 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
|
||||
import {
|
||||
IIssueFilterOptions,
|
||||
IIssueLabels,
|
||||
IProject,
|
||||
IState,
|
||||
IUserLite,
|
||||
TStateGroups,
|
||||
} from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
@@ -20,7 +27,9 @@ type Props = {
|
||||
clearAllFilters: (...args: any) => void;
|
||||
labels: IIssueLabels[] | undefined;
|
||||
members: IUserLite[] | undefined;
|
||||
states: IState[] | undefined;
|
||||
states?: IState[] | undefined;
|
||||
stateGroup?: string[] | undefined;
|
||||
project?: IProject[] | undefined;
|
||||
};
|
||||
|
||||
export const FiltersList: React.FC<Props> = ({
|
||||
@@ -30,6 +39,7 @@ export const FiltersList: React.FC<Props> = ({
|
||||
labels,
|
||||
members,
|
||||
states,
|
||||
project,
|
||||
}) => {
|
||||
if (!filters) return <></>;
|
||||
|
||||
@@ -155,6 +165,29 @@ export const FiltersList: React.FC<Props> = ({
|
||||
: key === "assignees"
|
||||
? filters.assignees?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
return (
|
||||
<div
|
||||
key={memberId}
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.display_name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "subscriber"
|
||||
? filters.subscriber?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -167,9 +200,7 @@ export const FiltersList: React.FC<Props> = ({
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
subscriber: filters.subscriber?.filter(
|
||||
(p: any) => p !== memberId
|
||||
),
|
||||
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -300,6 +331,30 @@ export const FiltersList: React.FC<Props> = ({
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "project"
|
||||
? filters.project?.map((projectId) => {
|
||||
const currentProject = project?.find((p) => p.id === projectId);
|
||||
console.log("currentProject", currentProject);
|
||||
console.log("currentProject", projectId);
|
||||
return (
|
||||
<p
|
||||
key={currentProject?.id}
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||
>
|
||||
<span>{currentProject?.name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
project: filters.project?.filter((p) => p !== projectId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
})
|
||||
: (filters[key] as any)?.join(", ")}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./date-filter-modal";
|
||||
export * from "./date-filter-select";
|
||||
export * from "./filters-list";
|
||||
export * from "./workspace-filters-list";
|
||||
export * from "./issues-view-filter";
|
||||
|
||||
@@ -277,35 +277,33 @@ export const IssuesFilterView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isArchivedIssues && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.type
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.type
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" && (
|
||||
|
||||
@@ -1,364 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||
// ui
|
||||
import { Avatar } from "components/ui";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import {
|
||||
IIssueLabels,
|
||||
IProject,
|
||||
IUserLite,
|
||||
IWorkspaceIssueFilterOptions,
|
||||
TStateGroups,
|
||||
} from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
type Props = {
|
||||
filters: Partial<IWorkspaceIssueFilterOptions>;
|
||||
setFilters: (updatedFilter: Partial<IWorkspaceIssueFilterOptions>) => void;
|
||||
clearAllFilters: (...args: any) => void;
|
||||
labels: IIssueLabels[] | undefined;
|
||||
members: IUserLite[] | undefined;
|
||||
stateGroup: string[] | undefined;
|
||||
project?: IProject[] | undefined;
|
||||
};
|
||||
|
||||
export const WorkspaceFiltersList: React.FC<Props> = ({
|
||||
filters,
|
||||
setFilters,
|
||||
clearAllFilters,
|
||||
labels,
|
||||
members,
|
||||
stateGroup,
|
||||
project,
|
||||
}) => {
|
||||
if (!filters) return <></>;
|
||||
|
||||
const nullFilters = Object.keys(filters).filter(
|
||||
(key) => filters[key as keyof IWorkspaceIssueFilterOptions] === null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-wrap items-center gap-2 text-xs">
|
||||
{Object.keys(filters).map((filterKey) => {
|
||||
const key = filterKey as keyof typeof filters;
|
||||
|
||||
if (filters[key] === null || (filters[key]?.length ?? 0) <= 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
|
||||
>
|
||||
<span className="capitalize text-custom-text-200">
|
||||
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||
</span>
|
||||
{filters[key] === null || (filters[key]?.length ?? 0) <= 0 ? (
|
||||
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
|
||||
) : Array.isArray(filters[key]) ? (
|
||||
<div className="space-x-2">
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{key === "state_group"
|
||||
? filters.state_group?.map((stateGroup) => {
|
||||
const group = stateGroup as TStateGroups;
|
||||
|
||||
return (
|
||||
<p
|
||||
key={group}
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||
style={{
|
||||
color: STATE_GROUP_COLORS[group],
|
||||
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<StateGroupIcon stateGroup={group} color={undefined} />
|
||||
</span>
|
||||
<span>{group}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
state_group: filters.state_group?.filter((g) => g !== group),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
})
|
||||
: key === "priority"
|
||||
? filters.priority?.map((priority: any) => (
|
||||
<p
|
||||
key={priority}
|
||||
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
|
||||
priority === "urgent"
|
||||
? "bg-red-500/20 text-red-500"
|
||||
: priority === "high"
|
||||
? "bg-orange-500/20 text-orange-500"
|
||||
: priority === "medium"
|
||||
? "bg-yellow-500/20 text-yellow-500"
|
||||
: priority === "low"
|
||||
? "bg-green-500/20 text-green-500"
|
||||
: "bg-custom-background-90 text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
<PriorityIcon priority={priority} />
|
||||
</span>
|
||||
<span>{priority === "null" ? "None" : priority}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
priority: filters.priority?.filter((p: any) => p !== priority),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</p>
|
||||
))
|
||||
: key === "assignees"
|
||||
? filters.assignees?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
return (
|
||||
<div
|
||||
key={memberId}
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.display_name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "subscriber"
|
||||
? filters.subscriber?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={memberId}
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.display_name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "created_by"
|
||||
? filters.created_by?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${memberId}-${key}`}
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.display_name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
created_by: filters.created_by?.filter(
|
||||
(p: any) => p !== memberId
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "labels"
|
||||
? filters.labels?.map((labelId: string) => {
|
||||
const label = labels?.find((l) => l.id === labelId);
|
||||
|
||||
if (!label) return null;
|
||||
const color = label.color !== "" ? label.color : "#0f172a";
|
||||
return (
|
||||
<div
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
|
||||
style={{
|
||||
color: color,
|
||||
backgroundColor: `${color}20`, // add 20% opacity
|
||||
}}
|
||||
key={labelId}
|
||||
>
|
||||
<div
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
<span>{label.name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
labels: filters.labels?.filter((l: any) => l !== labelId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon
|
||||
className="h-3 w-3"
|
||||
style={{
|
||||
color: color,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "start_date"
|
||||
? filters.start_date?.map((date: string) => {
|
||||
if (filters.start_date && filters.start_date.length <= 0) return null;
|
||||
|
||||
const splitDate = date.split(";");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={date}
|
||||
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
|
||||
>
|
||||
<div className="h-1.5 w-1.5 rounded-full" />
|
||||
<span className="capitalize">
|
||||
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||
</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
start_date: filters.start_date?.filter((d: any) => d !== date),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "target_date"
|
||||
? filters.target_date?.map((date: string) => {
|
||||
if (filters.target_date && filters.target_date.length <= 0) return null;
|
||||
|
||||
const splitDate = date.split(";");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={date}
|
||||
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
|
||||
>
|
||||
<div className="h-1.5 w-1.5 rounded-full" />
|
||||
<span className="capitalize">
|
||||
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||
</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
target_date: filters.target_date?.filter((d: any) => d !== date),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "project"
|
||||
? filters.project?.map((projectId) => {
|
||||
const currentProject = project?.find((p) => p.id === projectId);
|
||||
return (
|
||||
<p
|
||||
key={currentProject?.id}
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||
>
|
||||
<span>{currentProject?.name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
project: filters.project?.filter((p) => p !== projectId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
})
|
||||
: (filters[key] as any)?.join(", ")}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
[key]: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-x-1 capitalize">
|
||||
{filters[key as keyof typeof filters]}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
[key]: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearAllFilters}
|
||||
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
|
||||
>
|
||||
<span>Clear all filters</span>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,32 @@
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
|
||||
// next
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-dropdown
|
||||
import { useDropzone } from "react-dropzone";
|
||||
|
||||
// headless ui
|
||||
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||
|
||||
// services
|
||||
import fileService from "services/file.service";
|
||||
|
||||
// components
|
||||
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// hooks
|
||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { Input, PrimaryButton, SecondaryButton, Loader } from "components/ui";
|
||||
|
||||
const unsplashEnabled =
|
||||
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
|
||||
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "1";
|
||||
|
||||
const tabOptions = [
|
||||
{
|
||||
key: "unsplash",
|
||||
title: "Unsplash",
|
||||
},
|
||||
{
|
||||
key: "images",
|
||||
title: "Images",
|
||||
@@ -55,22 +64,8 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
||||
search: "",
|
||||
});
|
||||
|
||||
const { data: unsplashImages, error: unsplashError } = useSWR(
|
||||
`UNSPLASH_IMAGES_${searchParams}`,
|
||||
() => fileService.getUnsplashImages(searchParams),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: projectCoverImages } = useSWR(
|
||||
`PROJECT_COVER_IMAGES`,
|
||||
() => fileService.getProjectCoverImages(),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
}
|
||||
const { data: images } = useSWR(`UNSPLASH_IMAGES_${searchParams}`, () =>
|
||||
fileService.getUnsplashImages(1, searchParams)
|
||||
);
|
||||
|
||||
const imagePickerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -120,17 +115,18 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!unsplashImages || value !== null) return;
|
||||
|
||||
onChange(unsplashImages[0].urls.regular);
|
||||
}, [value, onChange, unsplashImages]);
|
||||
if (!images || value !== null) return;
|
||||
onChange(images[0].urls.regular);
|
||||
}, [value, onChange, images]);
|
||||
|
||||
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
|
||||
|
||||
if (!unsplashEnabled) return null;
|
||||
|
||||
return (
|
||||
<Popover className="relative z-[2]" ref={ref}>
|
||||
<Popover.Button
|
||||
className="rounded border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
||||
className="rounded-sm border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -145,19 +141,15 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-sm">
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
||||
<div
|
||||
ref={imagePickerRef}
|
||||
className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl"
|
||||
>
|
||||
<Tab.Group>
|
||||
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
||||
{tabOptions.map((tab) => {
|
||||
if (!unsplashImages && unsplashError && tab.key === "unsplash") return null;
|
||||
if (projectCoverImages && projectCoverImages.length === 0 && tab.key === "images")
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
||||
{tabOptions.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
@@ -168,106 +160,50 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
||||
>
|
||||
{tab.title}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</Tab.List>
|
||||
))}
|
||||
</Tab.List>
|
||||
</div>
|
||||
<Tab.Panels className="h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
|
||||
{(unsplashImages || !unsplashError) && (
|
||||
<Tab.Panel className="h-full w-full space-y-4 mt-4">
|
||||
<div className="flex gap-x-2">
|
||||
<Input
|
||||
name="search"
|
||||
className="text-sm"
|
||||
id="search"
|
||||
value={formData.search}
|
||||
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
||||
placeholder="Search for images"
|
||||
/>
|
||||
<PrimaryButton onClick={() => setSearchParams(formData.search)} size="sm">
|
||||
Search
|
||||
</PrimaryButton>
|
||||
<Tab.Panel className="h-full w-full space-y-4">
|
||||
<div className="flex gap-x-2 pt-7">
|
||||
<Input
|
||||
name="search"
|
||||
className="text-sm"
|
||||
id="search"
|
||||
value={formData.search}
|
||||
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
||||
placeholder="Search for images"
|
||||
/>
|
||||
<PrimaryButton onClick={() => setSearchParams(formData.search)} size="sm">
|
||||
Search
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
{images ? (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{images.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="relative col-span-2 aspect-video md:col-span-1"
|
||||
>
|
||||
<img
|
||||
src={image.urls.small}
|
||||
alt={image.alt_description}
|
||||
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
onChange(image.urls.regular);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{unsplashImages ? (
|
||||
unsplashImages.length > 0 ? (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{unsplashImages.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="relative col-span-2 aspect-video md:col-span-1"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
onChange(image.urls.regular);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={image.urls.small}
|
||||
alt={image.alt_description}
|
||||
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-custom-text-300 text-xs pt-7">
|
||||
No images found.
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="grid grid-cols-4 gap-4">
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
</Loader>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
)}
|
||||
{(!projectCoverImages || projectCoverImages.length !== 0) && (
|
||||
<Tab.Panel className="h-full w-full space-y-4 mt-4">
|
||||
{projectCoverImages ? (
|
||||
projectCoverImages.length > 0 ? (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{projectCoverImages.map((image, index) => (
|
||||
<div
|
||||
key={image}
|
||||
className="relative col-span-2 aspect-video md:col-span-1"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
onChange(image);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={`Default project cover image- ${index}`}
|
||||
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-custom-text-300 text-xs pt-7">
|
||||
No images found.
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="grid grid-cols-4 gap-4 pt-4">
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
</Loader>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
)}
|
||||
<Tab.Panel className="h-full w-full mt-4">
|
||||
) : (
|
||||
<div className="flex justify-center pt-20">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel className="h-full w-full pt-5">
|
||||
<div className="w-full h-full flex flex-col gap-y-2">
|
||||
<div className="flex items-center gap-3 w-full flex-1">
|
||||
<div
|
||||
|
||||
@@ -100,6 +100,7 @@ export const ThemeSwitch: React.FC<Props> = observer(
|
||||
}}
|
||||
input
|
||||
width="w-full"
|
||||
position="right"
|
||||
>
|
||||
{THEMES_OBJ.map(({ value, label, type, icon }) => (
|
||||
<CustomSelect.Option key={value} value={{ value, type }}>
|
||||
|
||||
@@ -48,7 +48,7 @@ const InlineInput = () => {
|
||||
export const BoardInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||
<>
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex flex-col border-[0.5px] border-custom-border-100 justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
{...props}
|
||||
>
|
||||
<InlineInput />
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useRouter } from "next/router";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// components
|
||||
import { CreateUpdateDraftIssueModal } from "components/issues";
|
||||
import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
@@ -58,7 +57,6 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||
const [isCreateDraftIssueModalOpen, setIsCreateDraftIssueModalOpen] = useState(false);
|
||||
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
@@ -77,7 +75,9 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const onCreateClick = () => {
|
||||
setIsInlineCreateIssueFormOpen(true);
|
||||
|
||||
const boardListElement = document.getElementById(`board-list-${groupTitle}`);
|
||||
|
||||
// timeout is needed because the animation
|
||||
@@ -93,32 +93,10 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const onCreateClick = () => {
|
||||
setIsInlineCreateIssueFormOpen(true);
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
const handleAddIssueToGroup = () => {
|
||||
if (isDraftIssuesPage) setIsCreateDraftIssueModalOpen(true);
|
||||
else if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
|
||||
else onCreateClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||
<CreateUpdateDraftIssueModal
|
||||
isOpen={isCreateDraftIssueModalOpen}
|
||||
handleClose={() => setIsCreateDraftIssueModalOpen(false)}
|
||||
prePopulateData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
[displayFilters?.group_by! === "labels" ? "labels_list" : displayFilters?.group_by!]:
|
||||
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
|
||||
}}
|
||||
/>
|
||||
|
||||
<BoardHeader
|
||||
addIssueToGroup={handleAddIssueToGroup}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
currentState={currentState}
|
||||
groupTitle={groupTitle}
|
||||
isCollapsed={isCollapsed}
|
||||
@@ -223,7 +201,6 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
<BoardInlineCreateIssueForm
|
||||
isOpen={isInlineCreateIssueFormOpen}
|
||||
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||
onSuccess={() => scrollToBottom()}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
@@ -237,38 +214,35 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
{displayFilters?.group_by !== "created_by" && (
|
||||
<div>
|
||||
{type === "issue"
|
||||
? !disableAddIssueOption &&
|
||||
!isDraftIssuesPage && (
|
||||
? !disableAddIssueOption && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
||||
onClick={() => {
|
||||
if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
|
||||
else onCreateClick();
|
||||
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
|
||||
addIssueToGroup();
|
||||
} else onCreateClick();
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</button>
|
||||
)
|
||||
: !disableUserActions &&
|
||||
!isDraftIssuesPage && (
|
||||
: !disableUserActions && (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="flex items-center gap-2 font-medium text-custom-primary outline-none whitespace-nowrap">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none whitespace-nowrap"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
position="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
if (isDraftIssuesPage) setIsCreateDraftIssueModalOpen(true);
|
||||
else if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
|
||||
else onCreateClick();
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => onCreateClick()}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
|
||||
@@ -369,12 +369,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{type && !isNotAllowed && (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded p-1 text-left text-xs duration-300 hover:bg-custom-background-80"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
|
||||
@@ -67,7 +67,7 @@ const InlineInput = () => {
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full pr-2 py-2.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -84,13 +84,13 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
|
||||
<>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`absolute top-10 transition-all z-20 w-full max-w-[calc(100%-1.25rem)] ${
|
||||
className={`absolute w-60 top-5 transition-all z-20 ${
|
||||
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
|
||||
} right-2.5`}
|
||||
} right-0`}
|
||||
>
|
||||
<InlineCreateIssueFormWrapper
|
||||
{...props}
|
||||
className="flex w-full px-1.5 border-[0.5px] border-custom-border-100 rounded z-50 items-center gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm transition-opacity"
|
||||
className="flex w-full p-1 px-1.5 rounded z-50 items-center gap-x-3 bg-custom-background-100 shadow-custom-shadow-sm transition-opacity"
|
||||
>
|
||||
<InlineInput />
|
||||
</InlineCreateIssueFormWrapper>
|
||||
|
||||
@@ -80,17 +80,23 @@ export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
|
||||
<CalendarInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
dependencies={[showWeekEnds]}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
target_date: date.date,
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
<div
|
||||
className="fixed top-0 left-0 z-50"
|
||||
style={{
|
||||
transform: `translate(${formPosition.x}px, ${formPosition.y}px)`,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<CalendarInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
dependencies={[showWeekEnds]}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
target_date: date.date,
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{totalIssues > 4 && (
|
||||
<button
|
||||
|
||||
@@ -48,7 +48,7 @@ const InlineInput = () => {
|
||||
export const GanttInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||
<>
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex py-3 px-4 border-[0.5px] border-custom-border-100 mr-2.5 items-center rounded gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
className="flex py-3 px-4 mr-2.5 items-center rounded gap-x-2 border bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
{...props}
|
||||
>
|
||||
<InlineInput />
|
||||
|
||||
@@ -197,7 +197,7 @@ export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
|
||||
)
|
||||
)
|
||||
.then(async (res) => {
|
||||
await mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
|
||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
|
||||
if (formData.cycle && formData.cycle !== "")
|
||||
await addIssueToCycle(
|
||||
workspaceSlug.toString(),
|
||||
|
||||
@@ -81,9 +81,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||
const isArchivedIssues = router.pathname?.split("/")?.[4] === "archived-issues";
|
||||
const isDraftIssues = router.asPath.includes("draft-issues");
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
@@ -483,6 +481,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
<CreateUpdateViewModal
|
||||
isOpen={createViewModal !== null}
|
||||
handleClose={() => setCreateViewModal(null)}
|
||||
viewType="project"
|
||||
preLoadedData={createViewModal}
|
||||
user={user}
|
||||
/>
|
||||
@@ -626,7 +625,6 @@ export const IssuesView: React.FC<Props> = ({
|
||||
params,
|
||||
properties,
|
||||
}}
|
||||
disableAddIssueOption={isArchivedIssues}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ const InlineInput = () => {
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 py-3 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -48,7 +48,7 @@ const InlineInput = () => {
|
||||
export const ListInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||
<>
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex border-[0.5px] border-t-0 border-custom-border-100 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-sm z-10"
|
||||
className="flex py-3 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-sm z-10"
|
||||
{...props}
|
||||
>
|
||||
<InlineInput />
|
||||
|
||||
@@ -328,7 +328,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</ContextMenu>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b-[0.5px] border-custom-border-100 bg-custom-background-100 last:border-b-0"
|
||||
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(true);
|
||||
@@ -352,7 +352,6 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
type="button"
|
||||
className="truncate text-[0.825rem] text-custom-text-100"
|
||||
onClick={() => {
|
||||
if (isArchivedIssues) return router.push(issuePath);
|
||||
if (!isDraftIssues) openPeekOverview(issue);
|
||||
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||
}}
|
||||
|
||||
@@ -13,7 +13,6 @@ import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
// components
|
||||
import { CreateUpdateDraftIssueModal } from "components/issues";
|
||||
import { SingleListIssue, ListInlineCreateIssueForm } from "components/core";
|
||||
// ui
|
||||
import { Avatar, CustomMenu } from "components/ui";
|
||||
@@ -76,7 +75,6 @@ export const SingleList: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
const [isDraftIssuesModalOpen, setIsDraftIssuesModalOpen] = useState(false);
|
||||
|
||||
const isMyIssuesPage = router.pathname.split("/")[3] === "my-issues";
|
||||
const isProfileIssuesPage = router.pathname.split("/")[2] === "profile";
|
||||
@@ -210,168 +208,149 @@ export const SingleList: React.FC<Props> = (props) => {
|
||||
if (!groupedIssues) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateDraftIssueModal
|
||||
isOpen={isDraftIssuesModalOpen}
|
||||
handleClose={() => setIsDraftIssuesModalOpen(false)}
|
||||
prePopulateData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
[displayFilters?.group_by! === "labels" ? "labels_list" : displayFilters?.group_by!]:
|
||||
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
|
||||
}}
|
||||
/>
|
||||
<Disclosure as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-custom-background-90">
|
||||
<Disclosure.Button>
|
||||
<div className="flex items-center gap-x-3">
|
||||
{displayFilters?.group_by !== null && (
|
||||
<div className="flex items-center">{getGroupIcon()}</div>
|
||||
)}
|
||||
{displayFilters?.group_by !== null ? (
|
||||
<h2
|
||||
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
|
||||
displayFilters?.group_by === "created_by" ? "" : "capitalize"
|
||||
}`}
|
||||
>
|
||||
{getGroupTitle()}
|
||||
</h2>
|
||||
) : (
|
||||
<h2 className="font-medium leading-5">All Issues</h2>
|
||||
)}
|
||||
<span className="text-custom-text-200 min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs">
|
||||
{groupedIssues[groupTitle as keyof IIssue].length}
|
||||
</span>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
{isArchivedIssues ? (
|
||||
""
|
||||
) : type === "issue" ? (
|
||||
!disableAddIssueOption && (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
)
|
||||
) : disableUserActions ? (
|
||||
""
|
||||
) : (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="flex cursor-pointer items-center">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</div>
|
||||
}
|
||||
position="right"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
{groupedIssues[groupTitle] ? (
|
||||
groupedIssues[groupTitle].length > 0 ? (
|
||||
groupedIssues[groupTitle].map((issue, index) => (
|
||||
<SingleListIssue
|
||||
key={issue.id}
|
||||
type={type}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
groupTitle={groupTitle}
|
||||
index={index}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
handleDraftIssueSelect={
|
||||
handleDraftIssueAction
|
||||
? () => handleDraftIssueAction(issue, "edit")
|
||||
: undefined
|
||||
}
|
||||
handleDraftIssueDelete={
|
||||
handleDraftIssueAction
|
||||
? () => handleDraftIssueAction(issue, "delete")
|
||||
: undefined
|
||||
}
|
||||
handleMyIssueOpen={handleMyIssueOpen}
|
||||
removeIssue={() => {
|
||||
if (removeIssue !== null && issue.bridge_id)
|
||||
removeIssue(issue.bridge_id, issue.id);
|
||||
}}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="bg-custom-background-100 px-4 py-2.5 text-sm text-custom-text-200">
|
||||
No issues.
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||
)}
|
||||
|
||||
<Disclosure as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-custom-background-90">
|
||||
<Disclosure.Button>
|
||||
<div className="flex items-center gap-x-3">
|
||||
{displayFilters?.group_by !== null && (
|
||||
<div className="flex items-center">{getGroupIcon()}</div>
|
||||
)}
|
||||
{displayFilters?.group_by !== null ? (
|
||||
<h2
|
||||
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
|
||||
displayFilters?.group_by === "created_by" ? "" : "capitalize"
|
||||
}`}
|
||||
>
|
||||
{getGroupTitle()}
|
||||
</h2>
|
||||
) : (
|
||||
<h2 className="font-medium leading-5">All Issues</h2>
|
||||
)}
|
||||
<span className="text-custom-text-200 min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs">
|
||||
{groupedIssues[groupTitle as keyof IIssue].length}
|
||||
</span>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
{isArchivedIssues ? (
|
||||
""
|
||||
) : type === "issue" ? (
|
||||
!disableAddIssueOption && (
|
||||
<ListInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen && !disableAddIssueOption}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
[displayFilters?.group_by!]: groupTitle,
|
||||
}}
|
||||
/>
|
||||
|
||||
{!disableAddIssueOption && !isCreateIssueFormOpen && (
|
||||
<div className="w-full bg-custom-background-100 px-6 py-3">
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
if (isDraftIssuesPage) setIsDraftIssuesModalOpen(true);
|
||||
else if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
|
||||
else setIsCreateIssueFormOpen(true);
|
||||
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
|
||||
addIssueToGroup();
|
||||
} else setIsCreateIssueFormOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
)
|
||||
) : disableUserActions ? (
|
||||
""
|
||||
) : (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="flex cursor-pointer items-center">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</div>
|
||||
}
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
{groupedIssues[groupTitle] ? (
|
||||
groupedIssues[groupTitle].length > 0 ? (
|
||||
groupedIssues[groupTitle].map((issue, index) => (
|
||||
<SingleListIssue
|
||||
key={issue.id}
|
||||
type={type}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
groupTitle={groupTitle}
|
||||
index={index}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
handleDraftIssueSelect={
|
||||
handleDraftIssueAction
|
||||
? () => handleDraftIssueAction(issue, "edit")
|
||||
: undefined
|
||||
}
|
||||
handleDraftIssueDelete={
|
||||
handleDraftIssueAction
|
||||
? () => handleDraftIssueAction(issue, "delete")
|
||||
: undefined
|
||||
}
|
||||
handleMyIssueOpen={handleMyIssueOpen}
|
||||
removeIssue={() => {
|
||||
if (removeIssue !== null && issue.bridge_id)
|
||||
removeIssue(issue.bridge_id, issue.id);
|
||||
}}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="bg-custom-background-100 px-4 py-2.5 text-sm text-custom-text-200">
|
||||
No issues.
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||
)}
|
||||
|
||||
<ListInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen && !disableAddIssueOption}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
[displayFilters?.group_by! === "labels"
|
||||
? "labels_list"
|
||||
: displayFilters?.group_by!]:
|
||||
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
|
||||
}}
|
||||
/>
|
||||
|
||||
{!disableAddIssueOption && !isCreateIssueFormOpen && !isDraftIssuesPage && (
|
||||
<div className="w-full bg-custom-background-100 px-6 py-3 border-b border-custom-border-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isDraftIssuesPage) setIsDraftIssuesModalOpen(true);
|
||||
else if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
|
||||
else setIsCreateIssueFormOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user