Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a249f28e1 | |||
| 1fadcdd1f4 | |||
| edd1f6e423 | |||
| 2bf7e63625 | |||
| eb78fd6088 | |||
| 202ecd21df | |||
| b2ac7b9ac6 | |||
| 51dff31926 | |||
| e89f152779 | |||
| 3c9f57f8f4 | |||
| 1bc859c68c | |||
| 11d57a5bf0 | |||
| 2980c7b00d | |||
| 5c6a59ba35 | |||
| a3ea7c8f10 | |||
| cb922fb113 | |||
| 06564ee856 | |||
| c7e6118804 | |||
| 069b8b3ed9 | |||
| 38a5b7bec0 | |||
| 236caaafe8 | |||
| a6d5eab634 | |||
| 8d76c96a6f | |||
| 97be4b60ae |
@@ -1,11 +1,10 @@
|
||||
|
||||
name: Branch Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
branches:
|
||||
- master
|
||||
- release
|
||||
- qa
|
||||
@@ -23,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
|
||||
# - name: Set Target Branch Name on PR close
|
||||
# if: ${{ github.event_name == 'pull_request' && github.event.action =='closed' }}
|
||||
# run: echo "TARGET_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV
|
||||
@@ -31,7 +30,7 @@ jobs:
|
||||
# - name: Set Target Branch Name on other than PR close
|
||||
# if: ${{ github.event_name == 'push' }}
|
||||
# run: echo "TARGET_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- uses: ASzc/change-string-case-action@v2
|
||||
id: gh_branch_upper_lower
|
||||
with:
|
||||
@@ -41,22 +40,22 @@ jobs:
|
||||
id: gh_branch_replace_slash
|
||||
with:
|
||||
source: ${{ steps.gh_branch_upper_lower.outputs.lowercase }}
|
||||
find: '/'
|
||||
replace: '-'
|
||||
find: "/"
|
||||
replace: "-"
|
||||
|
||||
- uses: mad9000/actions-find-and-replace-string@2
|
||||
id: gh_branch_replace_dot
|
||||
with:
|
||||
source: ${{ steps.gh_branch_replace_slash.outputs.value }}
|
||||
find: '.'
|
||||
replace: ''
|
||||
|
||||
find: "."
|
||||
replace: ""
|
||||
|
||||
- uses: mad9000/actions-find-and-replace-string@2
|
||||
id: gh_branch_clean
|
||||
with:
|
||||
source: ${{ steps.gh_branch_replace_dot.outputs.value }}
|
||||
find: '_'
|
||||
replace: ''
|
||||
find: "_"
|
||||
replace: ""
|
||||
- name: Uploading Proxy Source
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -77,7 +76,6 @@ jobs:
|
||||
!./nginx
|
||||
!./deploy
|
||||
!./space
|
||||
|
||||
- name: Uploading Space Source
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -89,11 +87,11 @@ jobs:
|
||||
!./deploy
|
||||
!./web
|
||||
outputs:
|
||||
gh_branch_name: ${{ steps.gh_branch_clean.outputs.value }}
|
||||
gh_branch_name: ${{ steps.gh_branch_clean.outputs.value }}
|
||||
|
||||
branch_build_push_frontend:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [ branch_build_and_push ]
|
||||
needs: [branch_build_and_push]
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
@@ -123,7 +121,7 @@ jobs:
|
||||
|
||||
branch_build_push_space:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [ branch_build_and_push ]
|
||||
needs: [branch_build_and_push]
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
@@ -153,7 +151,7 @@ jobs:
|
||||
|
||||
branch_build_push_backend:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [ branch_build_and_push ]
|
||||
needs: [branch_build_and_push]
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
@@ -183,7 +181,7 @@ jobs:
|
||||
|
||||
branch_build_push_proxy:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [ branch_build_and_push ]
|
||||
needs: [branch_build_and_push]
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
@@ -35,6 +35,7 @@ from plane.settings.redis import redis_instance
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
refresh = RefreshToken.for_user(user)
|
||||
@@ -152,7 +153,8 @@ class SignUpEndpoint(BaseAPIView):
|
||||
else 15,
|
||||
member=user,
|
||||
created_by_id=project_member_invite.created_by_id,
|
||||
) for project_member_invite in project_member_invites
|
||||
)
|
||||
for project_member_invite in project_member_invites
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
@@ -160,30 +162,17 @@ class SignUpEndpoint(BaseAPIView):
|
||||
workspace_member_invites.delete()
|
||||
project_member_invites.delete()
|
||||
|
||||
try:
|
||||
# Send Analytics
|
||||
if settings.ANALYTICS_BASE_API:
|
||||
_ = requests.post(
|
||||
settings.ANALYTICS_BASE_API,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||
},
|
||||
json={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"event_data": {
|
||||
"medium": "email",
|
||||
},
|
||||
"user": {"email": email, "id": str(user.id)},
|
||||
"device_ctx": {
|
||||
"ip": request.META.get("REMOTE_ADDR"),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||
},
|
||||
"event_type": "SIGN_UP",
|
||||
},
|
||||
)
|
||||
except RequestException as e:
|
||||
capture_exception(e)
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=True
|
||||
)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
|
||||
@@ -294,7 +283,8 @@ class SignInEndpoint(BaseAPIView):
|
||||
else 15,
|
||||
member=user,
|
||||
created_by_id=project_member_invite.created_by_id,
|
||||
) for project_member_invite in project_member_invites
|
||||
)
|
||||
for project_member_invite in project_member_invites
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
@@ -302,30 +292,17 @@ class SignInEndpoint(BaseAPIView):
|
||||
# Delete all the invites
|
||||
workspace_member_invites.delete()
|
||||
project_member_invites.delete()
|
||||
try:
|
||||
# Send Analytics
|
||||
if settings.ANALYTICS_BASE_API:
|
||||
_ = requests.post(
|
||||
settings.ANALYTICS_BASE_API,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||
},
|
||||
json={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"event_data": {
|
||||
"medium": "email",
|
||||
},
|
||||
"user": {"email": email, "id": str(user.id)},
|
||||
"device_ctx": {
|
||||
"ip": request.META.get("REMOTE_ADDR"),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||
},
|
||||
"event_type": "SIGN_IN",
|
||||
},
|
||||
)
|
||||
except RequestException as e:
|
||||
capture_exception(e)
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=False
|
||||
)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
data = {
|
||||
@@ -373,6 +350,13 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
||||
)
|
||||
and not (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
)
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
).exists()
|
||||
@@ -434,8 +418,7 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
|
||||
|
||||
ri.set(key, json.dumps(value), ex=expiry)
|
||||
|
||||
|
||||
current_site = request.META.get('HTTP_ORIGIN')
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
magic_link.delay(email, key, token, current_site)
|
||||
|
||||
return Response({"key": key}, status=status.HTTP_200_OK)
|
||||
@@ -467,30 +450,25 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
if str(token) == str(user_token):
|
||||
if User.objects.filter(email=email).exists():
|
||||
user = User.objects.get(email=email)
|
||||
try:
|
||||
# Send event to Jitsu for tracking
|
||||
if settings.ANALYTICS_BASE_API:
|
||||
_ = requests.post(
|
||||
settings.ANALYTICS_BASE_API,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||
},
|
||||
json={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"event_data": {
|
||||
"medium": "code",
|
||||
},
|
||||
"user": {"email": email, "id": str(user.id)},
|
||||
"device_ctx": {
|
||||
"ip": request.META.get("REMOTE_ADDR"),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||
},
|
||||
"event_type": "SIGN_IN",
|
||||
},
|
||||
)
|
||||
except RequestException as e:
|
||||
capture_exception(e)
|
||||
if not user.is_active:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=False
|
||||
)
|
||||
|
||||
else:
|
||||
user = User.objects.create(
|
||||
email=email,
|
||||
@@ -498,30 +476,18 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
password=make_password(uuid.uuid4().hex),
|
||||
is_password_autoset=True,
|
||||
)
|
||||
try:
|
||||
# Send event to Jitsu for tracking
|
||||
if settings.ANALYTICS_BASE_API:
|
||||
_ = requests.post(
|
||||
settings.ANALYTICS_BASE_API,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||
},
|
||||
json={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"event_data": {
|
||||
"medium": "code",
|
||||
},
|
||||
"user": {"email": email, "id": str(user.id)},
|
||||
"device_ctx": {
|
||||
"ip": request.META.get("REMOTE_ADDR"),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||
},
|
||||
"event_type": "SIGN_UP",
|
||||
},
|
||||
)
|
||||
except RequestException as e:
|
||||
capture_exception(e)
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=True
|
||||
)
|
||||
|
||||
user.is_active = True
|
||||
user.last_active = timezone.now()
|
||||
@@ -579,7 +545,8 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
else 15,
|
||||
member=user,
|
||||
created_by_id=project_member_invite.created_by_id,
|
||||
) for project_member_invite in project_member_invites
|
||||
)
|
||||
for project_member_invite in project_member_invites
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ from plane.db.models import (
|
||||
ProjectMemberInvite,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
from .base import BaseAPIView
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
@@ -143,19 +144,29 @@ class OauthEndpoint(BaseAPIView):
|
||||
"key", "value"
|
||||
)
|
||||
if (
|
||||
not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GOOGLE_CLIENT_ID",
|
||||
os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
(
|
||||
not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GOOGLE_CLIENT_ID",
|
||||
os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
)
|
||||
or not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GITHUB_CLIENT_ID",
|
||||
os.environ.get("GITHUB_CLIENT_ID"),
|
||||
)
|
||||
)
|
||||
or not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GITHUB_CLIENT_ID",
|
||||
os.environ.get("GITHUB_CLIENT_ID"),
|
||||
and not (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
)
|
||||
) and not WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
).exists():
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
@@ -275,29 +286,18 @@ class OauthEndpoint(BaseAPIView):
|
||||
"last_login_at": timezone.now(),
|
||||
},
|
||||
)
|
||||
try:
|
||||
if settings.ANALYTICS_BASE_API:
|
||||
_ = requests.post(
|
||||
settings.ANALYTICS_BASE_API,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||
},
|
||||
json={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"event_data": {
|
||||
"medium": f"oauth-{medium}",
|
||||
},
|
||||
"user": {"email": email, "id": str(user.id)},
|
||||
"device_ctx": {
|
||||
"ip": request.META.get("REMOTE_ADDR"),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||
},
|
||||
"event_type": "SIGN_IN",
|
||||
},
|
||||
)
|
||||
except RequestException as e:
|
||||
capture_exception(e)
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium=medium.upper(),
|
||||
first_time=False
|
||||
)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
|
||||
@@ -310,6 +310,23 @@ class OauthEndpoint(BaseAPIView):
|
||||
except User.DoesNotExist:
|
||||
## Signup Case
|
||||
|
||||
if (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
username = uuid.uuid4().hex
|
||||
|
||||
if "@" in email:
|
||||
@@ -401,29 +418,17 @@ class OauthEndpoint(BaseAPIView):
|
||||
workspace_member_invites.delete()
|
||||
project_member_invites.delete()
|
||||
|
||||
try:
|
||||
if settings.ANALYTICS_BASE_API:
|
||||
_ = requests.post(
|
||||
settings.ANALYTICS_BASE_API,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||
},
|
||||
json={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"event_data": {
|
||||
"medium": f"oauth-{medium}",
|
||||
},
|
||||
"user": {"email": email, "id": str(user.id)},
|
||||
"device_ctx": {
|
||||
"ip": request.META.get("REMOTE_ADDR"),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||
},
|
||||
"event_type": "SIGN_UP",
|
||||
},
|
||||
)
|
||||
except RequestException as e:
|
||||
capture_exception(e)
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium=medium.upper(),
|
||||
first_time=True
|
||||
)
|
||||
|
||||
SocialLoginConnection.objects.update_or_create(
|
||||
medium=medium,
|
||||
|
||||
@@ -183,7 +183,7 @@ class PageViewSet(BaseViewSet):
|
||||
|
||||
unarchive_archive_page_and_descendants(page_id, datetime.now())
|
||||
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def unarchive(self, request, slug, project_id, page_id):
|
||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import uuid
|
||||
|
||||
from posthog import Posthog
|
||||
from django.conf import settings
|
||||
|
||||
#third party imports
|
||||
from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
|
||||
@shared_task
|
||||
def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
|
||||
print(user, email, user_agent, ip, event_name, medium, first_time)
|
||||
try:
|
||||
posthog = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST)
|
||||
posthog.capture(
|
||||
email,
|
||||
event=event_name,
|
||||
properties={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"user": {"email": email, "id": str(user)},
|
||||
"device_ctx": {
|
||||
"ip": ip,
|
||||
"user_agent": user_agent,
|
||||
},
|
||||
"medium": medium,
|
||||
"first_time": first_time
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
@@ -322,4 +322,8 @@ ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
|
||||
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
|
||||
|
||||
# Use Minio settings
|
||||
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
||||
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
||||
|
||||
# Posthog settings
|
||||
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
|
||||
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
|
||||
@@ -326,20 +326,6 @@ def filter_start_target_date_issues(params, filter, method):
|
||||
return filter
|
||||
|
||||
|
||||
def filter_archived_issues(params, filter, method):
|
||||
archived = params.get("archived", "false")
|
||||
if archived == "true":
|
||||
filter["archived_at__isnull"] = False
|
||||
return filter
|
||||
|
||||
|
||||
def filter_draft_issues(params, filter, method):
|
||||
draft = params.get("draft", "false")
|
||||
if draft == "true":
|
||||
filter["is_draft"] = True
|
||||
return filter
|
||||
|
||||
|
||||
def issue_filters(query_params, method):
|
||||
filter = {}
|
||||
|
||||
@@ -367,8 +353,6 @@ def issue_filters(query_params, method):
|
||||
"sub_issue": filter_sub_issue_toggle,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
"start_target_date": filter_start_target_date_issues,
|
||||
"archived": filter_archived_issues,
|
||||
"draft": filter_draft_issues,
|
||||
}
|
||||
|
||||
for key, value in ISSUE_FILTER.items():
|
||||
|
||||
@@ -36,3 +36,4 @@ scout-apm==2.26.1
|
||||
openpyxl==3.1.2
|
||||
beautifulsoup4==4.12.2
|
||||
dj-database-url==2.1.0
|
||||
posthog==3.0.2
|
||||
@@ -44,7 +44,7 @@ export const SummaryPopover: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
{!sidePeekVisible && (
|
||||
<div
|
||||
className="hidden group-hover/summary-popover:block z-10 max-h-80 w-64 shadow-custom-shadow-rg rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3"
|
||||
className="hidden group-hover/summary-popover:block z-10 max-h-80 w-64 shadow-custom-shadow-rg rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 overflow-y-auto"
|
||||
ref={setPopperElement}
|
||||
style={summaryPopoverStyles.popper}
|
||||
{...summaryPopoverAttributes.popper}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
getIconStyling,
|
||||
getBadgeStyling,
|
||||
TBadgeVariant,
|
||||
TBadgeSizes,
|
||||
} from "./helper";
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: TBadgeVariant;
|
||||
size?: TBadgeSizes;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
appendIcon?: any;
|
||||
prependIcon?: any;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Badge = React.forwardRef<HTMLButtonElement, BadgeProps>((props, ref) => {
|
||||
const {
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
className = "",
|
||||
type = "button",
|
||||
loading = false,
|
||||
disabled = false,
|
||||
prependIcon = null,
|
||||
appendIcon = null,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const buttonStyle = getBadgeStyling(variant, size, disabled || loading);
|
||||
const buttonIconStyle = getIconStyling(size);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={`${buttonStyle} ${className}`}
|
||||
disabled={disabled || loading}
|
||||
{...rest}
|
||||
>
|
||||
{prependIcon && (
|
||||
<div className={buttonIconStyle}>
|
||||
{React.cloneElement(prependIcon, { strokeWidth: 2 })}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{appendIcon && (
|
||||
<div className={buttonIconStyle}>
|
||||
{React.cloneElement(appendIcon, { strokeWidth: 2 })}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
Badge.displayName = "plane-ui-badge";
|
||||
|
||||
export { Badge };
|
||||
@@ -0,0 +1,145 @@
|
||||
export type TBadgeVariant =
|
||||
| "primary"
|
||||
| "accent-primary"
|
||||
| "outline-primary"
|
||||
| "neutral"
|
||||
| "accent-neutral"
|
||||
| "outline-neutral"
|
||||
| "success"
|
||||
| "accent-success"
|
||||
| "outline-success"
|
||||
| "warning"
|
||||
| "accent-warning"
|
||||
| "outline-warning"
|
||||
| "destructive"
|
||||
| "accent-destructive"
|
||||
| "outline-destructive";
|
||||
|
||||
export type TBadgeSizes = "sm" | "md" | "lg" | "xl";
|
||||
|
||||
export interface IBadgeStyling {
|
||||
[key: string]: {
|
||||
default: string;
|
||||
hover: string;
|
||||
disabled: string;
|
||||
};
|
||||
}
|
||||
|
||||
enum badgeSizeStyling {
|
||||
sm = `px-2.5 py-1 font-medium text-xs rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
||||
md = `px-4 py-1.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
||||
lg = `px-4 py-2 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
||||
xl = `px-5 py-3 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
||||
}
|
||||
|
||||
enum badgeIconStyling {
|
||||
sm = "h-3 w-3 flex justify-center items-center overflow-hidden flex-shrink-0",
|
||||
md = "h-3.5 w-3.5 flex justify-center items-center overflow-hidden flex-shrink-0",
|
||||
lg = "h-4 w-4 flex justify-center items-center overflow-hidden flex-shrink-0",
|
||||
xl = "h-4 w-4 flex justify-center items-center overflow-hidden flex-shrink-0",
|
||||
}
|
||||
|
||||
export const badgeStyling: IBadgeStyling = {
|
||||
primary: {
|
||||
default: `text-white bg-custom-primary-100`,
|
||||
hover: `hover:bg-custom-primary-200`,
|
||||
disabled: `cursor-not-allowed !bg-custom-primary-60 hover:bg-custom-primary-60`,
|
||||
},
|
||||
"accent-primary": {
|
||||
default: `bg-custom-primary-10 text-custom-primary-100`,
|
||||
hover: `hover:bg-custom-primary-20 hover:text-custom-primary-200`,
|
||||
disabled: `cursor-not-allowed !text-custom-primary-60`,
|
||||
},
|
||||
"outline-primary": {
|
||||
default: `text-custom-primary-100 bg-custom-background-100 border border-custom-primary-100`,
|
||||
hover: `hover:border-custom-primary-80 hover:bg-custom-primary-10`,
|
||||
disabled: `cursor-not-allowed !text-custom-primary-60 !border-custom-primary-60 `,
|
||||
},
|
||||
|
||||
neutral: {
|
||||
default: `text-custom-background-100 bg-custom-text-100 border border-custom-border-200`,
|
||||
hover: `hover:bg-custom-text-200`,
|
||||
disabled: `cursor-not-allowed bg-custom-border-200 !text-custom-text-400`,
|
||||
},
|
||||
"accent-neutral": {
|
||||
default: `text-custom-text-200 bg-custom-background-80`,
|
||||
hover: `hover:bg-custom-border-200 hover:text-custom-text-100`,
|
||||
disabled: `cursor-not-allowed !text-custom-text-400`,
|
||||
},
|
||||
"outline-neutral": {
|
||||
default: `text-custom-text-200 bg-custom-background-100 border border-custom-border-200`,
|
||||
hover: `hover:text-custom-text-100 hover:bg-custom-border-200`,
|
||||
disabled: `cursor-not-allowed !text-custom-text-400`,
|
||||
},
|
||||
|
||||
success: {
|
||||
default: `text-white bg-green-500`,
|
||||
hover: `hover:bg-green-600`,
|
||||
disabled: `cursor-not-allowed !bg-green-300`,
|
||||
},
|
||||
"accent-success": {
|
||||
default: `text-green-500 bg-green-50`,
|
||||
hover: `hover:bg-green-100 hover:text-green-600`,
|
||||
disabled: `cursor-not-allowed !text-green-300`,
|
||||
},
|
||||
"outline-success": {
|
||||
default: `text-green-500 bg-custom-background-100 border border-green-500`,
|
||||
hover: `hover:text-green-600 hover:bg-green-50`,
|
||||
disabled: `cursor-not-allowed !text-green-300 border-green-300`,
|
||||
},
|
||||
|
||||
warning: {
|
||||
default: `text-white bg-amber-500`,
|
||||
hover: `hover:bg-amber-600`,
|
||||
disabled: `cursor-not-allowed !bg-amber-300`,
|
||||
},
|
||||
"accent-warning": {
|
||||
default: `text-amber-500 bg-amber-50`,
|
||||
hover: `hover:bg-amber-100 hover:text-amber-600`,
|
||||
disabled: `cursor-not-allowed !text-amber-300`,
|
||||
},
|
||||
"outline-warning": {
|
||||
default: `text-amber-500 bg-custom-background-100 border border-amber-500`,
|
||||
hover: `hover:text-amber-600 hover:bg-amber-50`,
|
||||
disabled: `cursor-not-allowed !text-amber-300 border-amber-300`,
|
||||
},
|
||||
|
||||
destructive: {
|
||||
default: `text-white bg-red-500`,
|
||||
hover: `hover:bg-red-600`,
|
||||
disabled: `cursor-not-allowed !bg-red-300`,
|
||||
},
|
||||
"accent-destructive": {
|
||||
default: `text-red-500 bg-red-50`,
|
||||
hover: `hover:bg-red-100 hover:text-red-600`,
|
||||
disabled: `cursor-not-allowed !text-red-300`,
|
||||
},
|
||||
"outline-destructive": {
|
||||
default: `text-red-500 bg-custom-background-100 border border-red-500`,
|
||||
hover: `hover:text-red-600 hover:bg-red-50`,
|
||||
disabled: `cursor-not-allowed !text-red-300 border-red-300`,
|
||||
},
|
||||
};
|
||||
|
||||
export const getBadgeStyling = (
|
||||
variant: TBadgeVariant,
|
||||
size: TBadgeSizes,
|
||||
disabled: boolean = false
|
||||
): string => {
|
||||
let _variant: string = ``;
|
||||
const currentVariant = badgeStyling[variant];
|
||||
|
||||
_variant = `${currentVariant.default} ${
|
||||
disabled ? currentVariant.disabled : currentVariant.hover
|
||||
}`;
|
||||
|
||||
let _size: string = ``;
|
||||
if (size) _size = badgeSizeStyling[size];
|
||||
return `${_variant} ${_size}`;
|
||||
};
|
||||
|
||||
export const getIconStyling = (size: TBadgeSizes): string => {
|
||||
let icon: string = ``;
|
||||
if (size) icon = badgeIconStyling[size];
|
||||
return icon;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./badge";
|
||||
@@ -51,11 +51,11 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
className={`flex items-center justify-between gap-1 text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
} ${customButtonClassName}`}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./avatar";
|
||||
export * from "./breadcrumbs";
|
||||
export * from "./badge";
|
||||
export * from "./button";
|
||||
export * from "./dropdowns";
|
||||
export * from "./form-fields";
|
||||
|
||||
@@ -10,16 +10,12 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
|
||||
@@ -28,7 +24,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
const [isDeactivating, setIsDeactivating] = useState(false);
|
||||
|
||||
const {
|
||||
user: { deactivateAccount },
|
||||
user: { deactivateAccount, signOut },
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
@@ -46,8 +42,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
const handleSwitchAccount = async () => {
|
||||
setSwitchingAccount(true);
|
||||
|
||||
await authService
|
||||
.signOut()
|
||||
await signOut()
|
||||
.then(() => {
|
||||
mutate("CURRENT_USER_DETAILS", null);
|
||||
setTheme("system");
|
||||
|
||||
@@ -1,61 +1,71 @@
|
||||
//react
|
||||
import { useState, Fragment, FC } from "react";
|
||||
//next
|
||||
import { useRouter } from "next/router";
|
||||
//ui
|
||||
import { Button } from "@plane/ui";
|
||||
//hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
//services
|
||||
import { APITokenService } from "services/api_token.service";
|
||||
//headless ui
|
||||
import { mutate } from "swr";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { APITokenService } from "services/api_token.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import { IApiToken } from "types/api_token";
|
||||
// fetch-keys
|
||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
tokenId?: string;
|
||||
onClose: () => void;
|
||||
tokenId: string;
|
||||
};
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
export const DeleteTokenModal: FC<Props> = (props) => {
|
||||
const { isOpen, handleClose, tokenId } = props;
|
||||
export const DeleteApiTokenModal: FC<Props> = (props) => {
|
||||
const { isOpen, onClose, tokenId } = props;
|
||||
// states
|
||||
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, tokenId: tokenIdFromQuery } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setDeleteLoading(false);
|
||||
};
|
||||
|
||||
const handleDeletion = () => {
|
||||
if (!workspaceSlug || (!tokenIdFromQuery && !tokenId)) return;
|
||||
|
||||
const token = tokenId || tokenIdFromQuery;
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
setDeleteLoading(true);
|
||||
|
||||
apiTokenService
|
||||
.deleteApiToken(workspaceSlug.toString(), token!.toString())
|
||||
.deleteApiToken(workspaceSlug.toString(), tokenId)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
message: "Token deleted successfully",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
message: "Token deleted successfully.",
|
||||
});
|
||||
router.replace(`/${workspaceSlug}/settings/api-tokens/`);
|
||||
|
||||
mutate<IApiToken[]>(
|
||||
API_TOKENS_LIST(workspaceSlug.toString()),
|
||||
(prevData) => (prevData ?? []).filter((token) => token.id !== tokenId),
|
||||
false
|
||||
);
|
||||
|
||||
handleClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch((err) =>
|
||||
setToastAlert({
|
||||
message: err?.message,
|
||||
type: "error",
|
||||
title: "Error",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setDeleteLoading(false);
|
||||
handleClose();
|
||||
});
|
||||
message: err?.message ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setDeleteLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -85,22 +95,24 @@ export const DeleteTokenModal: FC<Props> = (props) => {
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<div className="flex flex-col gap-3 p-6">
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
<div className="flex w-full items-center justify-start">
|
||||
<h3 className="text-xl font-semibold 2xl:text-2xl">Are you sure you want to revoke access?</h3>
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Are you sure you want to delete the token?
|
||||
</h3>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-base font-normal text-custom-text-400">
|
||||
Any applications Using this developer key will no longer have the access to Plane Data. This
|
||||
Action cannot be undone.
|
||||
<p className="text-sm text-custom-text-400">
|
||||
Any application using this token will no longer have the access to Plane data. This action cannot
|
||||
be undone.
|
||||
</p>
|
||||
</span>
|
||||
<div className="flex justify-end mt-2 gap-2">
|
||||
<Button variant="neutral-primary" onClick={handleClose} disabled={deleteLoading}>
|
||||
<Button variant="neutral-primary" onClick={handleClose} size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleDeletion} loading={deleteLoading} disabled={deleteLoading}>
|
||||
{deleteLoading ? "Revoking..." : "Revoke"}
|
||||
<Button variant="danger" onClick={handleDeletion} loading={deleteLoading} size="sm">
|
||||
{deleteLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// react
|
||||
import React from "react";
|
||||
// next
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import emptyApiTokens from "public/empty-state/api-token.svg";
|
||||
|
||||
export const APITokenEmptyState = () => {
|
||||
const router = useRouter();
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const ApiTokenEmptyState: React.FC<Props> = (props) => {
|
||||
const { onClick } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -17,19 +18,12 @@ export const APITokenEmptyState = () => {
|
||||
>
|
||||
<div className="text-center flex flex-col items-center w-full">
|
||||
<Image src={emptyApiTokens} className="w-52 sm:w-60" alt="empty" />
|
||||
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">No API Tokens</h6>
|
||||
{
|
||||
<p className="text-custom-text-300 mb-7 sm:mb-8">
|
||||
Create API tokens for safe and easy data sharing with external apps, maintaining control and security
|
||||
</p>
|
||||
}
|
||||
<Button
|
||||
className="flex items-center gap-1.5"
|
||||
onClick={() => {
|
||||
router.push(`${router.asPath}/create/`);
|
||||
}}
|
||||
>
|
||||
Add Token
|
||||
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">No API tokens</h6>
|
||||
<p className="text-custom-text-300 mb-7 sm:mb-8">
|
||||
Create API tokens for safe and easy data sharing with external apps, maintaining control and security.
|
||||
</p>
|
||||
<Button className="flex items-center gap-1.5" onClick={onClick}>
|
||||
Add token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useState, FC } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
// helpers
|
||||
import { addDays, renderDateFormat } from "helpers/date-time.helper";
|
||||
import { csvDownload } from "helpers/download.helper";
|
||||
// types
|
||||
import { IApiToken } from "types/api_token";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import { APITokenService } from "services/api_token.service";
|
||||
// components
|
||||
import { APITokenTitle } from "./token-title";
|
||||
import { APITokenDescription } from "./token-description";
|
||||
import { APITokenExpiry, EXPIRY_OPTIONS } from "./token-expiry";
|
||||
import { APITokenKeySection } from "./token-key-section";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
|
||||
interface APITokenFormProps {
|
||||
generatedToken: IApiToken | null | undefined;
|
||||
setGeneratedToken: Dispatch<SetStateAction<IApiToken | null | undefined>>;
|
||||
setDeleteTokenModal: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export interface APIFormFields {
|
||||
never_expires: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
export const APITokenForm: FC<APITokenFormProps> = (props) => {
|
||||
const { generatedToken, setGeneratedToken, setDeleteTokenModal } = props;
|
||||
// states
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [neverExpires, setNeverExpire] = useState<boolean>(false);
|
||||
const [focusTitle, setFocusTitle] = useState<boolean>(false);
|
||||
const [focusDescription, setFocusDescription] = useState<boolean>(false);
|
||||
const [selectedExpiry, setSelectedExpiry] = useState<number>(1);
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
// store
|
||||
const {
|
||||
theme: { sidebarCollapsed },
|
||||
} = useMobxStore();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
never_expires: false,
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
});
|
||||
|
||||
const getExpiryDate = (): string | null => {
|
||||
if (neverExpires === true) return null;
|
||||
return addDays({ date: new Date(), days: EXPIRY_OPTIONS[selectedExpiry].days }).toISOString();
|
||||
};
|
||||
|
||||
function renderExpiry(): string {
|
||||
return renderDateFormat(addDays({ date: new Date(), days: EXPIRY_OPTIONS[selectedExpiry].days }), true);
|
||||
}
|
||||
|
||||
const downloadSecretKey = (token: IApiToken) => {
|
||||
const csvData = {
|
||||
Label: token.label,
|
||||
Description: token.description,
|
||||
Expiry: renderDateFormat(token.expired_at ?? null),
|
||||
"Secret Key": token.token,
|
||||
};
|
||||
csvDownload(csvData, `Secret-key-${Date.now()}`);
|
||||
};
|
||||
|
||||
const generateToken = async (data: any) => {
|
||||
if (!workspaceSlug) return;
|
||||
setLoading(true);
|
||||
await apiTokenService
|
||||
.createApiToken(workspaceSlug.toString(), {
|
||||
label: data.title,
|
||||
description: data.description,
|
||||
expired_at: getExpiryDate(),
|
||||
})
|
||||
.then((res) => {
|
||||
setGeneratedToken(res);
|
||||
downloadSecretKey(res);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
message: err.message,
|
||||
type: "error",
|
||||
title: "Error",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(generateToken, (err) => {
|
||||
if (err.title) {
|
||||
setFocusTitle(true);
|
||||
}
|
||||
})}
|
||||
className={`${sidebarCollapsed ? "xl:w-[50%] lg:w-[60%] " : "w-[60%]"} mx-auto py-8`}
|
||||
>
|
||||
<div className="border-b border-custom-border-200 pb-4">
|
||||
<APITokenTitle
|
||||
generatedToken={generatedToken}
|
||||
control={control}
|
||||
errors={errors}
|
||||
focusTitle={focusTitle}
|
||||
setFocusTitle={setFocusTitle}
|
||||
setFocusDescription={setFocusDescription}
|
||||
/>
|
||||
{errors.title && focusTitle && <p className=" text-red-600">{errors.title.message}</p>}
|
||||
<APITokenDescription
|
||||
generatedToken={generatedToken}
|
||||
control={control}
|
||||
focusDescription={focusDescription}
|
||||
setFocusTitle={setFocusTitle}
|
||||
setFocusDescription={setFocusDescription}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!generatedToken && (
|
||||
<div className="mt-12">
|
||||
<>
|
||||
<APITokenExpiry
|
||||
neverExpires={neverExpires}
|
||||
selectedExpiry={selectedExpiry}
|
||||
setSelectedExpiry={setSelectedExpiry}
|
||||
setNeverExpire={setNeverExpire}
|
||||
renderExpiry={renderExpiry}
|
||||
control={control}
|
||||
/>
|
||||
<Button variant="primary" type="submit">
|
||||
{loading ? "generating..." : "Add Api key"}
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
<APITokenKeySection
|
||||
generatedToken={generatedToken}
|
||||
renderExpiry={renderExpiry}
|
||||
setDeleteTokenModal={setDeleteTokenModal}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// ui
|
||||
import { TextArea } from "@plane/ui";
|
||||
// types
|
||||
import { IApiToken } from "types/api_token";
|
||||
import type { APIFormFields } from "./index";
|
||||
|
||||
interface APITokenDescriptionProps {
|
||||
generatedToken: IApiToken | null | undefined;
|
||||
control: Control<APIFormFields, any>;
|
||||
focusDescription: boolean;
|
||||
setFocusTitle: Dispatch<SetStateAction<boolean>>;
|
||||
setFocusDescription: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const APITokenDescription: FC<APITokenDescriptionProps> = (props) => {
|
||||
const { generatedToken, control, focusDescription, setFocusTitle, setFocusDescription } = props;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field: { value, onChange } }) =>
|
||||
focusDescription ? (
|
||||
<TextArea
|
||||
id="description"
|
||||
name="description"
|
||||
autoFocus={true}
|
||||
onBlur={() => {
|
||||
setFocusDescription(false);
|
||||
}}
|
||||
value={value}
|
||||
defaultValue={value}
|
||||
onChange={onChange}
|
||||
placeholder="Description"
|
||||
className="mt-3"
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
onClick={() => {
|
||||
if (generatedToken != null) return;
|
||||
setFocusTitle(false);
|
||||
setFocusDescription(true);
|
||||
}}
|
||||
role="button"
|
||||
className={`${value.length === 0 ? "text-custom-text-400/60" : "text-custom-text-300"} text-lg pt-3`}
|
||||
>
|
||||
{value.length != 0 ? value : "Description"}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,111 +0,0 @@
|
||||
import { Dispatch, Fragment, SetStateAction, FC } from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
// types
|
||||
import { APIFormFields } from "./index";
|
||||
|
||||
interface APITokenExpiryProps {
|
||||
neverExpires: boolean;
|
||||
selectedExpiry: number;
|
||||
setSelectedExpiry: Dispatch<SetStateAction<number>>;
|
||||
setNeverExpire: Dispatch<SetStateAction<boolean>>;
|
||||
renderExpiry: () => string;
|
||||
control: Control<APIFormFields, any>;
|
||||
}
|
||||
|
||||
export const EXPIRY_OPTIONS = [
|
||||
{
|
||||
title: "7 Days",
|
||||
days: 7,
|
||||
},
|
||||
{
|
||||
title: "30 Days",
|
||||
days: 30,
|
||||
},
|
||||
{
|
||||
title: "1 Month",
|
||||
days: 30,
|
||||
},
|
||||
{
|
||||
title: "3 Months",
|
||||
days: 90,
|
||||
},
|
||||
{
|
||||
title: "1 Year",
|
||||
days: 365,
|
||||
},
|
||||
];
|
||||
|
||||
export const APITokenExpiry: FC<APITokenExpiryProps> = (props) => {
|
||||
const { neverExpires, selectedExpiry, setSelectedExpiry, setNeverExpire, renderExpiry, control } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu>
|
||||
<p className="text-sm font-medium mb-2"> Expiration Date</p>
|
||||
<Menu.Button className={"w-[40%]"} disabled={neverExpires}>
|
||||
<div className="py-3 w-full font-medium px-3 flex border border-custom-border-200 rounded-md justify-center items-baseline">
|
||||
<p className={`text-base ${neverExpires ? "text-custom-text-400/40" : ""}`}>
|
||||
{EXPIRY_OPTIONS[selectedExpiry].title.toLocaleLowerCase()}
|
||||
</p>
|
||||
<p className={`text-sm mr-auto ml-2 text-custom-text-400${neverExpires ? "/40" : ""}`}>
|
||||
({renderExpiry()})
|
||||
</p>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-10 overflow-y-scroll whitespace-nowrap rounded-sm max-h-36 border origin-top-right mt-1 overflow-auto min-w-[10rem] border-custom-border-100 p-1 shadow-lg focus:outline-none bg-custom-background-100">
|
||||
{EXPIRY_OPTIONS.map((option, index) => (
|
||||
<Menu.Item key={index}>
|
||||
{({ active }) => (
|
||||
<div className="py-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedExpiry(index);
|
||||
}}
|
||||
className={`w-full text-sm select-none truncate rounded px-3 py-1.5 text-left text-custom-text-300 hover:bg-custom-background-80 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
>
|
||||
{option.title}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
|
||||
<div className="mt-4 mb-6 flex items-center">
|
||||
<span className="text-sm font-medium"> Never Expires</span>
|
||||
<Controller
|
||||
control={control}
|
||||
name="never_expires"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ToggleSwitch
|
||||
className="ml-3"
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
setNeverExpire(val);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Dispatch, SetStateAction, FC } from "react";
|
||||
// icons
|
||||
import { Copy } from "lucide-react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IApiToken } from "types/api_token";
|
||||
|
||||
interface APITokenKeySectionProps {
|
||||
generatedToken: IApiToken | null | undefined;
|
||||
renderExpiry: () => string;
|
||||
setDeleteTokenModal: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const APITokenKeySection: FC<APITokenKeySectionProps> = (props) => {
|
||||
const { generatedToken, renderExpiry, setDeleteTokenModal } = props;
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
return generatedToken ? (
|
||||
<div className={`mt-${generatedToken ? "8" : "16"}`}>
|
||||
<p className="font-medium text-base pb-2">Api key created successfully</p>
|
||||
<p className="text-sm pb-4 w-[80%] text-custom-text-400/60">
|
||||
Save this API key somewhere safe. You will not be able to view it again once you close this page or reload this
|
||||
page.
|
||||
</p>
|
||||
<Button variant="neutral-primary" className="py-3 w-[85%] flex justify-between items-center">
|
||||
<p className="font-medium text-base">{generatedToken.token}</p>
|
||||
|
||||
<Copy
|
||||
size={18}
|
||||
color="#B9B9B9"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(generatedToken.token);
|
||||
setToastAlert({
|
||||
message: "The Secret key has been successfully copied to your clipboard",
|
||||
type: "success",
|
||||
title: "Copied to clipboard",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<p className="mt-2 text-sm text-custom-text-400/60">
|
||||
{generatedToken.expired_at ? "Expires on " + renderExpiry() : "Never Expires"}
|
||||
</p>
|
||||
<button
|
||||
className="border py-3 px-5 text-custom-primary-100 text-sm mt-8 rounded-md border-custom-primary-100 w-fit font-medium"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setDeleteTokenModal(true);
|
||||
}}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
import { Control, Controller, FieldErrors } from "react-hook-form";
|
||||
// ui
|
||||
import { Input } from "@plane/ui";
|
||||
// types
|
||||
import { IApiToken } from "types/api_token";
|
||||
import type { APIFormFields } from "./index";
|
||||
|
||||
interface APITokenTitleProps {
|
||||
generatedToken: IApiToken | null | undefined;
|
||||
errors: FieldErrors<APIFormFields>;
|
||||
control: Control<APIFormFields, any>;
|
||||
focusTitle: boolean;
|
||||
setFocusTitle: Dispatch<SetStateAction<boolean>>;
|
||||
setFocusDescription: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const APITokenTitle: FC<APITokenTitleProps> = (props) => {
|
||||
const { generatedToken, errors, control, focusTitle, setFocusTitle, setFocusDescription } = props;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name="title"
|
||||
rules={{
|
||||
required: "Title is required",
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) =>
|
||||
focusTitle ? (
|
||||
<Input
|
||||
id="title"
|
||||
name="title"
|
||||
type="text"
|
||||
inputSize="md"
|
||||
onBlur={() => {
|
||||
setFocusTitle(false);
|
||||
}}
|
||||
onError={() => {
|
||||
console.log("error");
|
||||
}}
|
||||
autoFocus
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={!!errors.title}
|
||||
placeholder="Title"
|
||||
className="resize-none text-xl w-full"
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
onClick={() => {
|
||||
if (generatedToken != null) return;
|
||||
setFocusDescription(false);
|
||||
setFocusTitle(true);
|
||||
}}
|
||||
role="button"
|
||||
className={`${value.length === 0 ? "text-custom-text-400/60" : ""} font-medium text-[24px]`}
|
||||
>
|
||||
{value.length != 0 ? value : "Api Title"}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "./modal";
|
||||
export * from "./delete-token-modal";
|
||||
export * from "./empty-state";
|
||||
export * from "./token-list-item";
|
||||
export * from "./form";
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { APITokenService } from "services/api_token.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CreateApiTokenForm, GeneratedTokenDetails } from "components/api-token";
|
||||
// helpers
|
||||
import { csvDownload } from "helpers/download.helper";
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IApiToken } from "types/api_token";
|
||||
// fetch-keys
|
||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
// services
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
export const CreateApiTokenModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
// states
|
||||
const [neverExpires, setNeverExpires] = useState<boolean>(false);
|
||||
const [generatedToken, setGeneratedToken] = useState<IApiToken | null | undefined>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
|
||||
setTimeout(() => {
|
||||
setNeverExpires(false);
|
||||
setGeneratedToken(null);
|
||||
}, 350);
|
||||
};
|
||||
|
||||
const downloadSecretKey = (data: IApiToken) => {
|
||||
const csvData = {
|
||||
Title: data.label,
|
||||
Description: data.description,
|
||||
Expiry: data.expired_at ? renderFormattedDate(data.expired_at) : "Never expires",
|
||||
"Secret key": data.token ?? "",
|
||||
};
|
||||
|
||||
csvDownload(csvData, `secret-key-${Date.now()}`);
|
||||
};
|
||||
|
||||
const handleCreateToken = async (data: Partial<IApiToken>) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
// make the request to generate the token
|
||||
await apiTokenService
|
||||
.createApiToken(workspaceSlug.toString(), data)
|
||||
.then((res) => {
|
||||
setGeneratedToken(res);
|
||||
downloadSecretKey(res);
|
||||
|
||||
mutate<IApiToken[]>(
|
||||
API_TOKENS_LIST(workspaceSlug.toString()),
|
||||
(prevData) => {
|
||||
if (!prevData) return;
|
||||
|
||||
return [res, ...prevData];
|
||||
},
|
||||
false
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
message: err.message,
|
||||
type: "error",
|
||||
title: "Error",
|
||||
});
|
||||
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={() => {}}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="h-full w-full grid place-items-center text-center p-4">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all px-4 sm:w-full sm:max-w-2xl">
|
||||
{generatedToken ? (
|
||||
<GeneratedTokenDetails handleClose={handleClose} tokenDetails={generatedToken} />
|
||||
) : (
|
||||
<CreateApiTokenForm
|
||||
handleClose={handleClose}
|
||||
neverExpires={neverExpires}
|
||||
toggleNeverExpires={() => setNeverExpires((prevData) => !prevData)}
|
||||
onSubmit={handleCreateToken}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,247 @@
|
||||
import { useState } from "react";
|
||||
import { add } from "date-fns";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Calendar } from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
// ui
|
||||
import { Button, CustomSelect, Input, TextArea, ToggleSwitch } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IApiToken } from "types/api_token";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
neverExpires: boolean;
|
||||
toggleNeverExpires: () => void;
|
||||
onSubmit: (data: Partial<IApiToken>) => Promise<void>;
|
||||
};
|
||||
|
||||
const EXPIRY_DATE_OPTIONS = [
|
||||
{
|
||||
key: "1_week",
|
||||
label: "1 week",
|
||||
value: { weeks: 1 },
|
||||
},
|
||||
{
|
||||
key: "1_month",
|
||||
label: "1 month",
|
||||
value: { months: 1 },
|
||||
},
|
||||
{
|
||||
key: "3_months",
|
||||
label: "3 months",
|
||||
value: { months: 3 },
|
||||
},
|
||||
{
|
||||
key: "1_year",
|
||||
label: "1 year",
|
||||
value: { years: 1 },
|
||||
},
|
||||
];
|
||||
|
||||
const defaultValues: Partial<IApiToken> = {
|
||||
label: "",
|
||||
description: "",
|
||||
expired_at: null,
|
||||
};
|
||||
|
||||
const getExpiryDate = (val: string): string | null => {
|
||||
const today = new Date();
|
||||
|
||||
const dateToAdd = EXPIRY_DATE_OPTIONS.find((option) => option.key === val)?.value;
|
||||
|
||||
if (dateToAdd) {
|
||||
const expiryDate = add(today, dateToAdd);
|
||||
|
||||
return renderFormattedDate(expiryDate);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
||||
const { handleClose, neverExpires, toggleNeverExpires, onSubmit } = props;
|
||||
// states
|
||||
const [customDate, setCustomDate] = useState<Date | null>(null);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// form
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useForm<IApiToken>({ defaultValues });
|
||||
|
||||
const handleFormSubmit = async (data: IApiToken) => {
|
||||
// if never expires is toggled off, and the user has not selected a custom date or a predefined date, show an error
|
||||
if (!neverExpires && (!data.expired_at || (data.expired_at === "custom" && !customDate)))
|
||||
return setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Please select an expiration date.",
|
||||
});
|
||||
|
||||
const payload: Partial<IApiToken> = {
|
||||
label: data.label,
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
// if never expires is toggled on, set expired_at to null
|
||||
if (neverExpires) payload.expired_at = null;
|
||||
// if never expires is toggled off, and the user has selected a custom date, set expired_at to the custom date
|
||||
else if (data.expired_at === "custom") payload.expired_at = renderFormattedPayloadDate(customDate ?? new Date());
|
||||
// if never expires is toggled off, and the user has selected a predefined date, set expired_at to the predefined date
|
||||
else {
|
||||
const expiryDate = getExpiryDate(data.expired_at ?? "");
|
||||
|
||||
if (expiryDate) payload.expired_at = renderFormattedPayloadDate(expiryDate);
|
||||
}
|
||||
|
||||
await onSubmit(payload).then(() => {
|
||||
reset(defaultValues);
|
||||
setCustomDate(null);
|
||||
});
|
||||
};
|
||||
|
||||
const today = new Date();
|
||||
const tomorrow = add(today, { days: 1 });
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Create token</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="label"
|
||||
rules={{
|
||||
required: "Title is required",
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
},
|
||||
validate: (val) => val.trim() !== "" || "Title is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.label)}
|
||||
placeholder="Token title"
|
||||
className="text-sm font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.label && <span className="text-xs text-red-500">{errors.label.message}</span>}
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TextArea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.description)}
|
||||
placeholder="Token description"
|
||||
className="text-sm h-24 w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expired_at"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const selectedOption = EXPIRY_DATE_OPTIONS.find((option) => option.key === value);
|
||||
|
||||
return (
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<div
|
||||
className={`flex items-center gap-2 border-[0.5px] border-custom-border-200 rounded py-1 px-2 ${
|
||||
neverExpires ? "text-custom-text-400" : ""
|
||||
}`}
|
||||
>
|
||||
<Calendar className="h-3 w-3" />
|
||||
{value === "custom"
|
||||
? "Custom date"
|
||||
: selectedOption
|
||||
? selectedOption.label
|
||||
: "Set expiration date"}
|
||||
</div>
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={neverExpires}
|
||||
>
|
||||
{EXPIRY_DATE_OPTIONS.map((option) => (
|
||||
<CustomSelect.Option key={option.key} value={option.key}>
|
||||
{option.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
<CustomSelect.Option value="custom">Custom</CustomSelect.Option>
|
||||
</CustomSelect>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{watch("expired_at") === "custom" && (
|
||||
<CustomDatePicker
|
||||
value={customDate}
|
||||
onChange={(date) => setCustomDate(date ? new Date(date) : null)}
|
||||
minDate={tomorrow}
|
||||
customInput={
|
||||
<div
|
||||
className={`flex items-center gap-2 py-1 px-2 text-xs cursor-pointer !rounded border-[0.5px] border-custom-border-200 !shadow-none !duration-0 ${
|
||||
customDate ? "w-[7.5rem]" : ""
|
||||
} ${neverExpires ? "text-custom-text-400 !cursor-not-allowed" : "hover:bg-custom-background-80"}`}
|
||||
>
|
||||
<Calendar className="h-3 w-3" />
|
||||
{customDate ? renderFormattedDate(customDate) : "Set date"}
|
||||
</div>
|
||||
}
|
||||
disabled={neverExpires}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!neverExpires && (
|
||||
<span className="text-xs text-custom-text-400">
|
||||
{watch("expired_at") === "custom"
|
||||
? customDate
|
||||
? `Expires ${renderFormattedDate(customDate)}`
|
||||
: null
|
||||
: watch("expired_at")
|
||||
? `Expires ${getExpiryDate(watch("expired_at") ?? "")}`
|
||||
: null}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center justify-between gap-2">
|
||||
<div className="flex cursor-pointer items-center gap-1.5" onClick={toggleNeverExpires}>
|
||||
<div className="flex cursor-pointer items-center justify-center">
|
||||
<ToggleSwitch value={neverExpires} onChange={() => {}} size="sm" />
|
||||
</div>
|
||||
<span className="text-xs">Never expires</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Discard
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Generating..." : "Generate token"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Copy } from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IApiToken } from "types/api_token";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
tokenDetails: IApiToken;
|
||||
};
|
||||
|
||||
export const GeneratedTokenDetails: React.FC<Props> = (props) => {
|
||||
const { handleClose, tokenDetails } = props;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const copyApiToken = (token: string) => {
|
||||
copyTextToClipboard(token).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Token copied to clipboard.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Key created</h3>
|
||||
<p className="text-sm text-custom-text-400">
|
||||
Copy and save this secret key in Plane Pages. You can{"'"}t see this key after you hit Close. A CSV file
|
||||
containing the key has been downloaded.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyApiToken(tokenDetails.token ?? "")}
|
||||
className="mt-4 w-full border-[0.5px] border-custom-border-200 py-2 px-3 flex items-center justify-between font-medium rounded-md text-sm outline-none"
|
||||
>
|
||||
{tokenDetails.token}
|
||||
<Tooltip tooltipContent="Copy secret key">
|
||||
<Copy className="h-4 w-4 text-custom-text-400" />
|
||||
</Tooltip>
|
||||
</button>
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<p className="text-custom-text-400 text-xs">
|
||||
{tokenDetails.expired_at ? `Expires ${renderFormattedDate(tokenDetails.expired_at)}` : "Never expires"}
|
||||
</p>
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./create-token-modal";
|
||||
export * from "./form";
|
||||
export * from "./generated-token-details";
|
||||
@@ -1,43 +1,58 @@
|
||||
import Link from "next/link";
|
||||
// helpers
|
||||
import { formatLongDateDistance, timeAgo } from "helpers/date-time.helper";
|
||||
// icons
|
||||
import { useState } from "react";
|
||||
import { XCircle } from "lucide-react";
|
||||
// components
|
||||
import { DeleteApiTokenModal } from "components/api-token";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate, timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IApiToken } from "types/api_token";
|
||||
|
||||
interface IApiTokenListItem {
|
||||
workspaceSlug: string | string[] | undefined;
|
||||
type Props = {
|
||||
token: IApiToken;
|
||||
}
|
||||
};
|
||||
|
||||
export const APITokenListItem = ({ token, workspaceSlug }: IApiTokenListItem) => (
|
||||
<Link href={`/${workspaceSlug}/settings/api-tokens/${token.id}`} key={token.id}>
|
||||
<div className="border-b flex flex-col relative justify-center items-start border-custom-border-200 py-5 hover:cursor-pointer">
|
||||
<XCircle className="absolute right-5 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto justify-self-center stroke-custom-text-400 h-[15px] w-[15px]" />
|
||||
<div className="flex items-center px-4">
|
||||
<span className="text-sm font-medium leading-6">{token.label}</span>
|
||||
<span
|
||||
className={`${
|
||||
token.is_active ? "bg-green-600/10 text-green-600" : "bg-custom-text-400/20 text-custom-text-400"
|
||||
} flex items-center px-2 h-4 rounded-sm max-h-fit ml-2 text-xs font-medium`}
|
||||
>
|
||||
{token.is_active ? "Active" : "Expired"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center px-4 w-full">
|
||||
{token.description.length != 0 && (
|
||||
<p className="text-sm mb-1 mr-3 font-medium leading-6 truncate max-w-[50%]">{token.description}</p>
|
||||
)}
|
||||
{
|
||||
export const ApiTokenListItem: React.FC<Props> = (props) => {
|
||||
const { token } = props;
|
||||
// states
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} />
|
||||
<div className="group relative border-b border-custom-border-200 flex flex-col justify-center py-3 px-4">
|
||||
<Tooltip tooltipContent="Delete token">
|
||||
<button
|
||||
onClick={() => setDeleteModalOpen(true)}
|
||||
className="hidden group-hover:grid absolute right-4 place-items-center"
|
||||
>
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className="flex items-center w-4/5">
|
||||
<h5 className="text-sm font-medium truncate">{token.label}</h5>
|
||||
<span
|
||||
className={`${
|
||||
token.is_active ? "bg-green-500/10 text-green-500" : "bg-custom-background-80 text-custom-text-400"
|
||||
} flex items-center px-2 h-4 rounded-sm max-h-fit ml-2 text-xs font-medium`}
|
||||
>
|
||||
{token.is_active ? "Active" : "Expired"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center w-full mt-1">
|
||||
{token.description.trim() !== "" && (
|
||||
<p className="text-sm mb-1 break-words max-w-[70%]">{token.description}</p>
|
||||
)}
|
||||
<p className="text-xs mb-1 leading-6 text-custom-text-400">
|
||||
{token.is_active
|
||||
? token.expired_at === null
|
||||
? "Never Expires"
|
||||
: `Expires in ${formatLongDateDistance(token.expired_at!)}`
|
||||
: timeAgo(token.expired_at)}
|
||||
? token.expired_at
|
||||
? `Expires ${renderFormattedDate(token.expired_at!)}`
|
||||
: "Never expires"
|
||||
: `Expired ${timeAgo(token.expired_at)}`}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
|
||||
|
||||
const {
|
||||
commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal },
|
||||
trackEvent: { setTrackElement }
|
||||
} = useMobxStore();
|
||||
|
||||
return (
|
||||
@@ -22,6 +23,7 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("COMMAND_PALETTE")
|
||||
toggleCreateCycleModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
|
||||
@@ -62,6 +62,7 @@ export const CommandModal: React.FC = observer(() => {
|
||||
toggleCreateIssueModal,
|
||||
toggleCreateProjectModal,
|
||||
},
|
||||
trackEvent: { setTrackElement }
|
||||
} = useMobxStore();
|
||||
|
||||
// router
|
||||
@@ -273,6 +274,7 @@ export const CommandModal: React.FC = observer(() => {
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("COMMAND_PALETTE");
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
className="focus:bg-custom-background-80"
|
||||
@@ -290,6 +292,7 @@ export const CommandModal: React.FC = observer(() => {
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("COMMAND_PALETTE");
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
|
||||
@@ -33,6 +33,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
commandPalette,
|
||||
theme: { toggleSidebar },
|
||||
user: { currentUser },
|
||||
trackEvent: { setTrackElement },
|
||||
} = useMobxStore();
|
||||
const {
|
||||
toggleCommandPaletteModal,
|
||||
@@ -54,8 +55,22 @@ export const CommandPalette: FC = observer(() => {
|
||||
toggleBulkDeleteIssueModal,
|
||||
isDeleteIssueModalOpen,
|
||||
toggleDeleteIssueModal,
|
||||
|
||||
createIssueStoreType,
|
||||
} = commandPalette;
|
||||
|
||||
const isAnyModalOpen = Boolean(
|
||||
isCreateIssueModalOpen ||
|
||||
isCreateCycleModalOpen ||
|
||||
isCreatePageModalOpen ||
|
||||
isCreateProjectModalOpen ||
|
||||
isCreateModuleModalOpen ||
|
||||
isCreateViewModalOpen ||
|
||||
isShortcutModalOpen ||
|
||||
isBulkDeleteIssueModalOpen ||
|
||||
isDeleteIssueModalOpen
|
||||
);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: issueDetails } = useSWR(
|
||||
@@ -110,10 +125,12 @@ export const CommandPalette: FC = observer(() => {
|
||||
e.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
} else {
|
||||
} else if (!isAnyModalOpen) {
|
||||
if (keyPressed === "c") {
|
||||
setTrackElement("SHORTCUT_KEY");
|
||||
toggleCreateIssueModal(true);
|
||||
} else if (keyPressed === "p") {
|
||||
setTrackElement("SHORTCUT_KEY");
|
||||
toggleCreateProjectModal(true);
|
||||
} else if (keyPressed === "h") {
|
||||
toggleShortcutModal(true);
|
||||
@@ -145,6 +162,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
toggleCreateIssueModal,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
isAnyModalOpen,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -208,6 +226,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
prePopulateData={
|
||||
cycleId ? { cycle: cycleId.toString() } : moduleId ? { module: moduleId.toString() } : undefined
|
||||
}
|
||||
currentStore={createIssueStoreType}
|
||||
/>
|
||||
|
||||
{issueId && issueDetails && (
|
||||
|
||||
@@ -14,6 +14,8 @@ import { FileService } from "services/file.service";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { Button, Input, Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "constants/common";
|
||||
|
||||
const tabOptions = [
|
||||
{
|
||||
@@ -58,8 +60,10 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { workspace: workspaceStore } = useMobxStore();
|
||||
const { currentWorkspace: workspaceDetails } = workspaceStore;
|
||||
const {
|
||||
workspace: { currentWorkspace },
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
|
||||
const { data: unsplashImages, error: unsplashError } = useSWR(
|
||||
`UNSPLASH_IMAGES_${searchParams}`,
|
||||
@@ -86,7 +90,7 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
||||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"],
|
||||
},
|
||||
maxSize: 5 * 1024 * 1024,
|
||||
maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -112,7 +116,7 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
||||
|
||||
if (isUnsplashImage) return;
|
||||
|
||||
if (oldValue && workspaceDetails) fileService.deleteFile(workspaceDetails.id, oldValue);
|
||||
if (oldValue && currentWorkspace) fileService.deleteFile(currentWorkspace.id, oldValue);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./bulk-delete-issues-modal";
|
||||
export * from "./existing-issues-list-modal";
|
||||
export * from "./gpt-assistant-modal";
|
||||
export * from "./image-upload-modal";
|
||||
export * from "./link-modal";
|
||||
export * from "./user-image-upload-modal";
|
||||
export * from "./workspace-image-upload-modal";
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { UserCircle2 } from "lucide-react";
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "constants/common";
|
||||
|
||||
type Props = {
|
||||
handleDelete?: () => void;
|
||||
isOpen: boolean;
|
||||
isRemoving: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (url: string) => void;
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
||||
const { value, onSuccess, isOpen, onClose, isRemoving, handleDelete } = props;
|
||||
// states
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
|
||||
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"],
|
||||
},
|
||||
maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
setImage(null);
|
||||
setIsImageUploading(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
console.log("Submit triggered");
|
||||
|
||||
if (!image) return;
|
||||
|
||||
console.log("Inside submit");
|
||||
|
||||
setIsImageUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("asset", image);
|
||||
formData.append("attributes", JSON.stringify({}));
|
||||
|
||||
fileService
|
||||
.uploadUserFile(formData)
|
||||
.then((res) => {
|
||||
const imageUrl = res.asset;
|
||||
|
||||
onSuccess(imageUrl);
|
||||
setImage(null);
|
||||
|
||||
if (value) fileService.deleteUserFile(value);
|
||||
})
|
||||
.catch((err) =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setIsImageUploading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-30" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Upload Image
|
||||
</Dialog.Title>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`relative grid h-80 w-80 cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-custom-primary focus:ring-offset-2 ${
|
||||
(image === null && isDragActive) || !value
|
||||
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{image !== null || (value && value !== "") ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-0 right-0 z-40 translate-x-1/2 -translate-y-1/2 rounded bg-custom-background-90 px-2 py-0.5 text-xs font-medium text-custom-text-200"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<img
|
||||
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
||||
alt="image"
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<UserCircle2 className="mx-auto h-16 w-16 text-custom-text-200" />
|
||||
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
||||
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input {...getInputProps()} type="text" />
|
||||
</div>
|
||||
</div>
|
||||
{fileRejections.length > 0 && (
|
||||
<p className="text-sm text-red-500">
|
||||
{fileRejections[0].errors[0].code === "file-too-large"
|
||||
? "The image size cannot exceed 5 MB."
|
||||
: "Please upload a file in a valid format."}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="my-4 text-custom-text-200 text-sm">
|
||||
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
{handleDelete && (
|
||||
<Button variant="danger" size="sm" onClick={handleDelete} disabled={!value}>
|
||||
{isRemoving ? "Removing..." : "Remove"}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleSubmit}
|
||||
disabled={!image}
|
||||
loading={isImageUploading}
|
||||
>
|
||||
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
||||
+51
-51
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
@@ -7,89 +7,89 @@ import { Transition, Dialog } from "@headlessui/react";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { UserCircle2 } from "lucide-react";
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "constants/common";
|
||||
|
||||
type Props = {
|
||||
value?: string | null;
|
||||
onClose: () => void;
|
||||
handleRemove?: () => void;
|
||||
isOpen: boolean;
|
||||
onSuccess: (url: string) => void;
|
||||
isRemoving: boolean;
|
||||
handleDelete: () => void;
|
||||
userImage?: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (url: string) => void;
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const ImageUploadModal: React.FC<Props> = observer((props) => {
|
||||
const { value, onSuccess, isOpen, onClose, isRemoving, handleDelete, userImage } = props;
|
||||
|
||||
export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
||||
const { value, onSuccess, isOpen, onClose, isRemoving, handleRemove } = props;
|
||||
// states
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { workspace: workspaceStore } = useMobxStore();
|
||||
const { currentWorkspace: workspaceDetails } = workspaceStore;
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setImage(acceptedFiles[0]);
|
||||
}, []);
|
||||
const {
|
||||
workspace: { currentWorkspace },
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
|
||||
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"],
|
||||
},
|
||||
maxSize: 5 * 1024 * 1024,
|
||||
maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
setImage(null);
|
||||
setIsImageUploading(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!image || (!workspaceSlug && router.pathname != "/onboarding")) return;
|
||||
if (!image || (!workspaceSlug && router.pathname !== "/onboarding")) return;
|
||||
|
||||
setIsImageUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("asset", image);
|
||||
formData.append("attributes", JSON.stringify({}));
|
||||
|
||||
if (userImage) {
|
||||
fileService
|
||||
.uploadUserFile(formData)
|
||||
.then((res) => {
|
||||
const imageUrl = res.asset;
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
onSuccess(imageUrl);
|
||||
setIsImageUploading(false);
|
||||
setImage(null);
|
||||
fileService
|
||||
.uploadFile(workspaceSlug.toString(), formData)
|
||||
.then((res) => {
|
||||
const imageUrl = res.asset;
|
||||
|
||||
if (value) fileService.deleteUserFile(value);
|
||||
onSuccess(imageUrl);
|
||||
setImage(null);
|
||||
|
||||
if (value && currentWorkspace) fileService.deleteFile(currentWorkspace.id, value);
|
||||
})
|
||||
.catch((err) =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
} else
|
||||
fileService
|
||||
.uploadFile(workspaceSlug as string, formData)
|
||||
.then((res) => {
|
||||
const imageUrl = res.asset;
|
||||
onSuccess(imageUrl);
|
||||
setIsImageUploading(false);
|
||||
setImage(null);
|
||||
|
||||
if (value && workspaceDetails) fileService.deleteFile(workspaceDetails.id, value);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setImage(null);
|
||||
onClose();
|
||||
)
|
||||
.finally(() => setIsImageUploading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -172,11 +172,11 @@ export const ImageUploadModal: React.FC<Props> = observer((props) => {
|
||||
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button variant="danger" size="sm" onClick={handleDelete} disabled={!value}>
|
||||
{handleRemove && (
|
||||
<Button variant="danger" size="sm" onClick={handleRemove} disabled={!value}>
|
||||
{isRemoving ? "Removing..." : "Remove"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
@@ -33,7 +33,7 @@ export interface ICyclesBoardCard {
|
||||
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
const { cycle, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
const { cycle: cycleStore, trackEvent: { setTrackElement } } = useMobxStore();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
// states
|
||||
@@ -119,6 +119,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteModal(true);
|
||||
setTrackElement("CYCLE_PAGE_BOARD_LAYOUT");
|
||||
};
|
||||
|
||||
const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -252,7 +253,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>Delete module</span>
|
||||
<span>Delete cycle</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
|
||||
@@ -38,7 +38,7 @@ type TCyclesListItem = {
|
||||
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
const { cycle, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
const { cycle: cycleStore, trackEvent: { setTrackElement } } = useMobxStore();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
// states
|
||||
@@ -119,6 +119,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteModal(true);
|
||||
setTrackElement("CYCLE_PAGE_LIST_LAYOUT");
|
||||
};
|
||||
|
||||
const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface ICyclesList {
|
||||
export const CyclesList: FC<ICyclesList> = observer((props) => {
|
||||
const { cycles, filter, workspaceSlug, projectId } = props;
|
||||
|
||||
const { commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const { commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -57,7 +57,11 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
|
||||
<button
|
||||
type="button"
|
||||
className="text-custom-primary-100 text-sm outline-none"
|
||||
onClick={() => commandPaletteStore.toggleCreateCycleModal(true)}
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLES_PAGE_EMPTY-STATE");
|
||||
commandPaletteStore.toggleCreateCycleModal(true)
|
||||
}
|
||||
}
|
||||
>
|
||||
Create a new cycle
|
||||
</button>
|
||||
|
||||
@@ -24,26 +24,40 @@ interface ICycleDelete {
|
||||
export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
const { cycle: cycleStore, trackEvent: { postHogEventTracker } } = useMobxStore();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
// states
|
||||
const [loader, setLoader] = useState(false);
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query;
|
||||
const { cycleId, peekCycle } = router.query;
|
||||
|
||||
const formSubmit = async () => {
|
||||
setLoader(true);
|
||||
if (cycle?.id)
|
||||
try {
|
||||
await cycleStore.removeCycle(workspaceSlug, projectId, cycle?.id);
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Cycle deleted successfully.",
|
||||
await cycleStore.removeCycle(workspaceSlug, projectId, cycle?.id).then((res) => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Cycle deleted successfully.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"CYCLE_DELETE",
|
||||
{
|
||||
state: "SUCCESS"
|
||||
}
|
||||
);
|
||||
}).catch((error) => {
|
||||
postHogEventTracker(
|
||||
"CYCLE_DELETE",
|
||||
{
|
||||
state: "FAILED"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (cycleId) router.replace(`/${workspaceSlug}/projects/${projectId}/cycles`);
|
||||
if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
|
||||
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
|
||||
@@ -24,7 +24,7 @@ const cycleService = new CycleService();
|
||||
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
const { cycle: cycleStore, trackEvent: { postHogEventTracker } } = useMobxStore();
|
||||
// states
|
||||
const [activeProject, setActiveProject] = useState<string>(projectId);
|
||||
// toast
|
||||
@@ -35,12 +35,19 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
const selectedProjectId = payload.project ?? projectId.toString();
|
||||
await cycleStore
|
||||
.createCycle(workspaceSlug, selectedProjectId, payload)
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Cycle created successfully.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"CYCLE_CREATE",
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
@@ -48,6 +55,12 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
title: "Error!",
|
||||
message: "Error in creating cycle. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"CYCLE_CREATE",
|
||||
{
|
||||
state: "FAILED",
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
||||
|
||||
const { cycle: cycleDetailsStore } = useMobxStore();
|
||||
const { cycle: cycleDetailsStore, trackEvent: { setTrackElement, postHogEventTracker } } = useMobxStore();
|
||||
|
||||
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
|
||||
|
||||
@@ -74,8 +74,27 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
cycleService
|
||||
.patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data)
|
||||
.then(() => mutate(CYCLE_DETAILS(cycleId as string)))
|
||||
.catch((e) => console.log(e));
|
||||
.then((res) => {
|
||||
mutate(CYCLE_DETAILS(cycleId as string));
|
||||
postHogEventTracker(
|
||||
"CYCLE_UPDATE",
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS"
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
postHogEventTracker(
|
||||
"CYCLE_UPDATE",
|
||||
{
|
||||
state: "FAILED"
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
@@ -83,7 +102,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Cycle link copied to clipboard",
|
||||
title: "Link Copied!",
|
||||
message: "Cycle link copied to clipboard.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -112,13 +132,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
const handleStartDateChange = async (date: string) => {
|
||||
setValue("start_date", date);
|
||||
if (
|
||||
watch("start_date") &&
|
||||
watch("end_date") &&
|
||||
watch("start_date") !== "" &&
|
||||
watch("end_date") &&
|
||||
watch("start_date") !== ""
|
||||
) {
|
||||
if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") {
|
||||
if (!isDateGreaterThanToday(`${watch("end_date")}`)) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@@ -186,13 +200,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const handleEndDateChange = async (date: string) => {
|
||||
setValue("end_date", date);
|
||||
|
||||
if (
|
||||
watch("start_date") &&
|
||||
watch("end_date") &&
|
||||
watch("start_date") !== "" &&
|
||||
watch("end_date") &&
|
||||
watch("start_date") !== ""
|
||||
) {
|
||||
if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") {
|
||||
if (!isDateGreaterThanToday(`${watch("end_date")}`)) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@@ -296,10 +304,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
cycleDetails.total_issues === 0
|
||||
? "0 Issue"
|
||||
: cycleDetails.total_issues === cycleDetails.completed_issues
|
||||
? cycleDetails.total_issues > 1
|
||||
? `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||
? cycleDetails.total_issues > 1
|
||||
? `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -329,7 +337,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
{!isCompleted && (
|
||||
<CustomMenu width="lg" placement="bottom-end" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
|
||||
<CustomMenu.MenuItem onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_SIDEBAR");
|
||||
setCycleDeleteModal(true)
|
||||
}
|
||||
}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>Delete cycle</span>
|
||||
@@ -379,6 +391,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
setTrackElement("CYCLE_PAGE_SIDEBAR_START_DATE_BUTTON");
|
||||
handleStartDateChange(val);
|
||||
}
|
||||
}}
|
||||
@@ -414,6 +427,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
setTrackElement("CYCLE_PAGE_SIDEBAR_END_DATE_BUTTON");
|
||||
handleEndDateChange(val);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -33,14 +34,13 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
const {
|
||||
cycle: cycleStore,
|
||||
cycleIssueFilters: cycleIssueFiltersStore,
|
||||
projectIssuesFilter: projectIssueFiltersStore,
|
||||
project: { currentProjectDetails },
|
||||
projectMember: { projectMembers },
|
||||
projectLabel: { projectLabels },
|
||||
projectState: projectStateStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
|
||||
trackEvent: { setTrackElement },
|
||||
cycleIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
@@ -190,7 +190,14 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button onClick={() => commandPaletteStore.toggleCreateIssueModal(true)} size="sm" prependIcon={<Plus />}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
<button
|
||||
|
||||
@@ -14,7 +14,7 @@ export const CyclesHeader: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store
|
||||
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
return (
|
||||
@@ -51,7 +51,11 @@ export const CyclesHeader: FC = observer(() => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => commandPaletteStore.toggleCreateCycleModal(true)}
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLES_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateCycleModal(true);
|
||||
}
|
||||
}
|
||||
>
|
||||
Add Cycle
|
||||
</Button>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -33,11 +34,11 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
const {
|
||||
module: moduleStore,
|
||||
projectIssuesFilter: projectIssueFiltersStore,
|
||||
project: projectStore,
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
projectLabel: { projectLabels },
|
||||
moduleIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
@@ -190,7 +191,14 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button onClick={() => commandPaletteStore.toggleCreateIssueModal(true)} size="sm" prependIcon={<Plus />}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("MODULE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
<button
|
||||
|
||||
@@ -19,7 +19,7 @@ export const ProfileSettingsHeader: FC<IProfileSettingHeader> = (props) => {
|
||||
type="text"
|
||||
label="My Profile"
|
||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||
link={`/me/profile`}
|
||||
link="/profile"
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
|
||||
</Breadcrumbs>
|
||||
|
||||
@@ -1,20 +1,74 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
// ui
|
||||
import { Breadcrumbs, LayersIcon } from "@plane/ui";
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
|
||||
export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
const {
|
||||
project: { currentProjectDetails },
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectDraftIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues });
|
||||
},
|
||||
[workspaceSlug, projectId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property);
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
@@ -44,6 +98,37 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels ?? undefined}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -31,6 +32,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
projectState: projectStateStore,
|
||||
inbox: inboxStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
// issue filters
|
||||
projectIssuesFilter: { issueFilters, updateFilters },
|
||||
projectIssues: {},
|
||||
@@ -198,7 +200,14 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button onClick={() => commandPaletteStore.toggleCreateIssueModal(true)} size="sm" prependIcon={<Plus />}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const ProjectsHeader = observer(() => {
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
// store
|
||||
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: {setTrackElement} } = useMobxStore();
|
||||
|
||||
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : [];
|
||||
|
||||
@@ -41,7 +41,10 @@ export const ProjectsHeader = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button prependIcon={<Plus />} size="sm" onClick={() => commandPaletteStore.toggleCreateProjectModal(true)}>
|
||||
<Button prependIcon={<Plus />} size="sm" onClick={() => {
|
||||
setTrackElement("PROJECTS_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateProjectModal(true)
|
||||
}}>
|
||||
Add Project
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ export const UserProfileHeader = () => (
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" label="Activity Overview" link="/me/profile" />
|
||||
<Breadcrumbs.BreadcrumbItem type="text" label="Activity Overview" link="/profile" />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,11 @@ import githubWhiteImage from "/public/logos/github-white.png";
|
||||
// components
|
||||
import { ProductUpdatesModal } from "components/common";
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export const WorkspaceDashboardHeader = () => {
|
||||
const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false);
|
||||
const { trackEvent: { postHogEventTracker } } = useMobxStore();
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
||||
|
||||
const { inboxIssueDetails: inboxIssueDetailsStore } = useMobxStore();
|
||||
const { inboxIssueDetails: inboxIssueDetailsStore, trackEvent: { postHogEventTracker } } = useMobxStore();
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -70,6 +70,21 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}`);
|
||||
handleClose();
|
||||
} else reset(defaultValues);
|
||||
postHogEventTracker(
|
||||
"ISSUE_CREATE",
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS"
|
||||
}
|
||||
);
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
postHogEventTracker(
|
||||
"ISSUE_CREATE",
|
||||
{
|
||||
state: "FAILED"
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -172,7 +187,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
>
|
||||
<span className="text-xs">Create more</span>
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
||||
<ToggleSwitch value={createMore} onChange={() => { }} size="md" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={() => handleClose()}>
|
||||
|
||||
@@ -9,17 +9,16 @@ import useToast from "hooks/use-toast";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export interface IInstanceOpenAIForm {
|
||||
export interface IInstanceAIForm {
|
||||
config: IFormattedInstanceConfiguration;
|
||||
}
|
||||
|
||||
export interface OpenAIFormValues {
|
||||
OPENAI_API_BASE: string;
|
||||
export interface AIFormValues {
|
||||
OPENAI_API_KEY: string;
|
||||
GPT_ENGINE: string;
|
||||
}
|
||||
|
||||
export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
|
||||
const { config } = props;
|
||||
// store
|
||||
const { instance: instanceStore } = useMobxStore();
|
||||
@@ -30,16 +29,15 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<OpenAIFormValues>({
|
||||
} = useForm<AIFormValues>({
|
||||
defaultValues: {
|
||||
OPENAI_API_BASE: config["OPENAI_API_BASE"],
|
||||
OPENAI_API_KEY: config["OPENAI_API_KEY"],
|
||||
GPT_ENGINE: config["GPT_ENGINE"],
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: OpenAIFormValues) => {
|
||||
const payload: Partial<OpenAIFormValues> = { ...formData };
|
||||
const onSubmit = async (formData: AIFormValues) => {
|
||||
const payload: Partial<AIFormValues> = { ...formData };
|
||||
|
||||
await instanceStore
|
||||
.updateInstanceConfigurations(payload)
|
||||
@@ -47,64 +45,15 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Open AI Settings updated successfully",
|
||||
message: "AI Settings updated successfully",
|
||||
})
|
||||
)
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 m-8 w-4/5">
|
||||
<div className="pb-2 mb-2 border-b border-custom-border-100">
|
||||
<div className="text-custom-text-100 font-medium text-lg">OpenAI</div>
|
||||
<div className="text-custom-text-300 font-normal text-sm">
|
||||
AI is everywhere make use it as much as you can! <a href="#" className="text-custom-primary-100">Learn more.</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">OpenAI API Base</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="OPENAI_API_BASE"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="OPENAI_API_BASE"
|
||||
name="OPENAI_API_BASE"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.OPENAI_API_BASE)}
|
||||
placeholder="OpenAI API Base"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">OpenAI API Key</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="OPENAI_API_KEY"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="OPENAI_API_KEY"
|
||||
name="OPENAI_API_KEY"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.OPENAI_API_KEY)}
|
||||
placeholder="OpenAI API Key"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-3 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">GPT Engine</h4>
|
||||
<Controller
|
||||
@@ -119,11 +68,54 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GPT_ENGINE)}
|
||||
placeholder="GPT Engine"
|
||||
placeholder="gpt-3.5-turbo"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
Choose an OpenAI engine.{" "}
|
||||
<a
|
||||
href="https://platform.openai.com/docs/models/overview"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">API Key</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="OPENAI_API_KEY"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="OPENAI_API_KEY"
|
||||
name="OPENAI_API_KEY"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.OPENAI_API_KEY)}
|
||||
placeholder="sk-asddassdfasdefqsdfasd23das3dasdcasd"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
You will find your API key{" "}
|
||||
<a
|
||||
href="https://platform.openai.com/api-keys"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -132,6 +124,6 @@ export const InstanceOpenAIForm: FC<IInstanceOpenAIForm> = (props) => {
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -31,6 +31,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
// form data
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<EmailFormValues>({
|
||||
@@ -60,11 +61,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 m-8 w-4/5">
|
||||
<div className="pb-2 mb-2 border-b border-custom-border-100">
|
||||
<div className="text-custom-text-100 font-medium text-lg">Email</div>
|
||||
<div className="text-custom-text-300 font-normal text-sm">Email related settings.</div>
|
||||
</div>
|
||||
<>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Host</h4>
|
||||
@@ -80,7 +77,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.EMAIL_HOST)}
|
||||
placeholder="Email Host"
|
||||
placeholder="email.google.com"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
@@ -101,7 +98,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.EMAIL_PORT)}
|
||||
placeholder="Email Port"
|
||||
placeholder="8080"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
@@ -123,7 +120,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.EMAIL_HOST_USER)}
|
||||
placeholder="Username"
|
||||
placeholder="getitdone@projectplane.so"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
@@ -139,7 +136,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
<Input
|
||||
id="EMAIL_HOST_PASSWORD"
|
||||
name="EMAIL_HOST_PASSWORD"
|
||||
type="text"
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
@@ -152,45 +149,55 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8 pt-4">
|
||||
<div>
|
||||
<div className="text-custom-text-100 font-medium text-sm">Enable TLS</div>
|
||||
<div className="w-full lg:w-1/2 flex flex-col px-1 gap-y-8">
|
||||
<div className="flex items-center gap-8 pt-4 mr-8">
|
||||
<div className="grow">
|
||||
<div className="text-custom-text-100 font-medium text-sm">
|
||||
Turn TLS {Boolean(parseInt(watch("EMAIL_USE_TLS"))) ? "off" : "on"}
|
||||
</div>
|
||||
<div className="text-custom-text-300 font-normal text-xs">Use this if your email domain supports TLS.</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Controller
|
||||
control={control}
|
||||
name="EMAIL_USE_TLS"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(value))}
|
||||
onChange={() => {
|
||||
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="EMAIL_USE_TLS"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(value))}
|
||||
onChange={() => {
|
||||
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8 pt-4">
|
||||
<div>
|
||||
<div className="text-custom-text-100 font-medium text-sm">Enable SSL</div>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="EMAIL_USE_SSL"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(value))}
|
||||
onChange={() => {
|
||||
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-8 pt-4 mr-8">
|
||||
<div className="grow">
|
||||
<div className="text-custom-text-100 font-medium text-sm">
|
||||
Turn SSL {Boolean(parseInt(watch("EMAIL_USE_SSL"))) ? "off" : "on"}
|
||||
</div>
|
||||
<div className="text-custom-text-300 font-normal text-xs">
|
||||
Most email domains support SSL. Use this to secure comms between this instance and your users.
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Controller
|
||||
control={control}
|
||||
name="EMAIL_USE_SSL"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(value))}
|
||||
onChange={() => {
|
||||
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -199,6 +206,6 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface IInstanceGeneralForm {
|
||||
|
||||
export interface GeneralFormValues {
|
||||
instance_name: string;
|
||||
is_telemetry_enabled: boolean;
|
||||
// is_telemetry_enabled: boolean;
|
||||
}
|
||||
|
||||
export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
@@ -31,7 +31,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
} = useForm<GeneralFormValues>({
|
||||
defaultValues: {
|
||||
instance_name: instance.instance_name,
|
||||
is_telemetry_enabled: instance.is_telemetry_enabled,
|
||||
// is_telemetry_enabled: instance.is_telemetry_enabled,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -51,13 +51,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 m-8">
|
||||
<div className="pb-2 mb-2 border-b border-custom-border-100">
|
||||
<div className="text-custom-text-100 font-medium text-lg">General</div>
|
||||
<div className="text-custom-text-300 font-normal text-sm">
|
||||
The usual things like your mail, name of instance and other stuff.
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Name of instance</h4>
|
||||
@@ -106,7 +100,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8 pt-4">
|
||||
{/* <div className="flex items-center gap-12 pt-4">
|
||||
<div>
|
||||
<div className="text-custom-text-100 font-medium text-sm">Share anonymous usage instance</div>
|
||||
<div className="text-custom-text-300 font-normal text-xs">
|
||||
@@ -120,13 +114,13 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
render={({ field: { value, onChange } }) => <ToggleSwitch value={value} onChange={onChange} size="sm" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="flex items-center py-1">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,8 +56,8 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
||||
const originURL = typeof window !== "undefined" ? window.location.origin : "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-3 justify-between gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Client ID</h4>
|
||||
<Controller
|
||||
@@ -72,11 +72,22 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GITHUB_CLIENT_ID)}
|
||||
placeholder="Github Client ID"
|
||||
placeholder="70a44354520df8bd9bcd"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
You will get this from your{" "}
|
||||
<a
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub OAuth application settings.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Client Secret</h4>
|
||||
@@ -92,14 +103,23 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GITHUB_CLIENT_SECRET)}
|
||||
placeholder="Github Client Secret"
|
||||
placeholder="9b0050f94ec1b744e32ce79ea4ffacd40d4119cb"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
Your client secret is also found in your{" "}
|
||||
<a
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub OAuth application settings.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Origin URL</h4>
|
||||
<Button
|
||||
@@ -117,16 +137,26 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
||||
<p className="font-medium text-sm">{originURL}</p>
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
</Button>
|
||||
<p className="text-xs text-custom-text-400/60">*paste this URL in your Github console.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center p-2">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
We will auto-generate this. Paste this into the Authorization callback URL field{" "}
|
||||
<a
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,8 +56,8 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
||||
const originURL = typeof window !== "undefined" ? window.location.origin : "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-3 justify-between gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Client ID</h4>
|
||||
<Controller
|
||||
@@ -72,11 +72,22 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GOOGLE_CLIENT_ID)}
|
||||
placeholder="Google Client ID"
|
||||
placeholder="840195096245-0p2tstej9j5nc4l8o1ah2dqondscqc1g.apps.googleusercontent.com"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
Your client ID lives in your Google API Console.{" "}
|
||||
<a
|
||||
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Client Secret</h4>
|
||||
@@ -92,14 +103,23 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.GOOGLE_CLIENT_SECRET)}
|
||||
placeholder="Google Client Secret"
|
||||
placeholder="GOCShX-ADp4cI0kPqav1gGCBg5bE02E"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
Your client secret should also be in your Google API Console.{" "}
|
||||
<a
|
||||
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Origin URL</h4>
|
||||
<Button
|
||||
@@ -117,16 +137,26 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
||||
<p className="font-medium text-sm">{originURL}</p>
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
</Button>
|
||||
<p className="text-xs text-custom-text-400/60">*paste this URL in your Google developer console.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center p-2">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
We will auto-generate this. Paste this into your Authorized JavaScript origins field. For this OAuth client{" "}
|
||||
<a
|
||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { FC } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// types
|
||||
import { IFormattedInstanceConfiguration } from "types/instance";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export interface IInstanceImageConfigForm {
|
||||
config: IFormattedInstanceConfiguration;
|
||||
}
|
||||
|
||||
export interface ImageConfigFormValues {
|
||||
UNSPLASH_ACCESS_KEY: string;
|
||||
}
|
||||
|
||||
export const InstanceImageConfigForm: FC<IInstanceImageConfigForm> = (props) => {
|
||||
const { config } = props;
|
||||
// store
|
||||
const { instance: instanceStore } = useMobxStore();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
// form data
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<ImageConfigFormValues>({
|
||||
defaultValues: {
|
||||
UNSPLASH_ACCESS_KEY: config["UNSPLASH_ACCESS_KEY"],
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: ImageConfigFormValues) => {
|
||||
const payload: Partial<ImageConfigFormValues> = { ...formData };
|
||||
|
||||
await instanceStore
|
||||
.updateInstanceConfigurations(payload)
|
||||
.then(() =>
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Image Configuration Settings updated successfully",
|
||||
})
|
||||
)
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 items-center justify-between gap-x-16 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Access key from your Unsplash account</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="UNSPLASH_ACCESS_KEY"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="UNSPLASH_ACCESS_KEY"
|
||||
name="UNSPLASH_ACCESS_KEY"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.UNSPLASH_ACCESS_KEY)}
|
||||
placeholder="oXgq-sdfadsaeweqasdfasdf3234234rassd"
|
||||
className="rounded-md font-medium w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
You will find your access key in your Unsplash developer console.{" "}
|
||||
<a
|
||||
href="https://unsplash.com/documentation#creating-a-developer-account"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center py-1">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -3,41 +3,35 @@ import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Cog, LogIn, LogOut, Settings, UserCircle2 } from "lucide-react";
|
||||
import { mutate } from "swr";
|
||||
// components
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
// ui
|
||||
import { Avatar, Tooltip } from "@plane/ui";
|
||||
|
||||
// Static Data
|
||||
const profileLinks = (workspaceSlug: string, userId: string) => [
|
||||
{
|
||||
name: "View profile",
|
||||
icon: UserCircle2,
|
||||
link: `/${workspaceSlug}/profile/${userId}`,
|
||||
},
|
||||
const PROFILE_LINKS = [
|
||||
{
|
||||
key: "settings",
|
||||
name: "Settings",
|
||||
icon: Settings,
|
||||
link: `/me/profile`,
|
||||
link: `/profile`,
|
||||
},
|
||||
];
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
export const InstanceSidebarDropdown = observer(() => {
|
||||
const router = useRouter();
|
||||
// store
|
||||
const {
|
||||
theme: { sidebarCollapsed },
|
||||
workspace: { workspaceSlug },
|
||||
user: { currentUser, currentUserSettings },
|
||||
user: { signOut, currentUser, currentUserSettings },
|
||||
} = useMobxStore();
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -51,8 +45,7 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
"";
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await authService
|
||||
.signOut()
|
||||
await signOut()
|
||||
.then(() => {
|
||||
mutate("CURRENT_USER_DETAILS", null);
|
||||
setTheme("system");
|
||||
@@ -68,28 +61,21 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-3 gap-y-2 px-4 py-4">
|
||||
<div className="flex items-center gap-x-2 gap-y-2 px-4 pt-3 pb-2 mb-2 border border-custom-sidebar-border-200">
|
||||
<div className="w-full h-full truncate">
|
||||
<div
|
||||
className={`flex flex-grow items-center gap-x-2 rounded p-1 truncate ${
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`}
|
||||
>
|
||||
<Cog className="h-5 w-5 text-custom-text-200" />
|
||||
<div className={`flex-shrink-0 flex items-center justify-center h-7 w-7 rounded bg-custom-sidebar-background-80`}>
|
||||
<UserCog2 className="h-6 w-6 text-custom-text-200" />
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && <h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
<Menu.Button className="flex gap-4 place-items-center outline-none">
|
||||
{!sidebarCollapsed && (
|
||||
<Tooltip position="bottom-left" tooltipContent="Go back to your workspace">
|
||||
{!sidebarCollapsed && (
|
||||
<div className="flex w-full gap-2">
|
||||
<h4 className="grow text-custom-text-200 font-medium text-base truncate">God Mode</h4>
|
||||
<Tooltip position="bottom-left" tooltipContent="Exit God Mode">
|
||||
<div className="flex-shrink-0">
|
||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||
<a>
|
||||
@@ -98,7 +84,14 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
</Link>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
<Menu.Button className="grid place-items-center outline-none">
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={currentUser?.avatar}
|
||||
@@ -118,13 +111,13 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 origin-top-left rounded-md
|
||||
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none"
|
||||
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 rounded-md
|
||||
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-100 shadow-lg text-xs outline-none"
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||
{profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
|
||||
<Menu.Item key={index} as="button" type="button">
|
||||
{PROFILE_LINKS.map((link) => (
|
||||
<Menu.Item key={link.key} as="button" type="button">
|
||||
<Link href={link.link}>
|
||||
<a className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
||||
<link.icon className="h-4 w-4 stroke-[1.5]" />
|
||||
@@ -149,8 +142,8 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
<div className="p-2 pb-0">
|
||||
<Menu.Item as="button" type="button" className="w-full">
|
||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-10 hover:bg-custom-primary-20">
|
||||
Normal Mode
|
||||
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-100/20 hover:bg-custom-primary-100/30">
|
||||
Exit God Mode
|
||||
</a>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
@@ -11,26 +11,32 @@ const INSTANCE_ADMIN_LINKS = [
|
||||
{
|
||||
Icon: Cog,
|
||||
name: "General",
|
||||
description: "General settings here",
|
||||
href: `/admin`,
|
||||
description: "Identify your instances and get key details",
|
||||
href: `/god-mode`,
|
||||
},
|
||||
{
|
||||
Icon: Mail,
|
||||
name: "Email",
|
||||
description: "Email related settings will go here",
|
||||
href: `/admin/email`,
|
||||
description: "Set up emails to your users",
|
||||
href: `/god-mode/email`,
|
||||
},
|
||||
{
|
||||
Icon: Lock,
|
||||
name: "Authorization",
|
||||
description: "Autorization",
|
||||
href: `/admin/authorization`,
|
||||
name: "SSO and OAuth",
|
||||
description: "Configure your Google and GitHub SSOs",
|
||||
href: `/god-mode/authorization`,
|
||||
},
|
||||
{
|
||||
Icon: BrainCog,
|
||||
name: "OpenAI",
|
||||
description: "OpenAI configurations",
|
||||
href: `/admin/openai`,
|
||||
name: "Artificial intelligence",
|
||||
description: "Configure your OpenAI creds",
|
||||
href: `/god-mode/ai`,
|
||||
},
|
||||
{
|
||||
Icon: Image,
|
||||
name: "Images in Plane",
|
||||
description: "Allow third-party image libraries",
|
||||
href: `/god-mode/image`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -68,7 +74,9 @@ export const InstanceAdminSidebarMenu = () => {
|
||||
{item.name}
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs ${isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-300"}`}
|
||||
className={`text-[10px] ${
|
||||
isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-300"
|
||||
}`}
|
||||
>
|
||||
{item.description}
|
||||
</span>
|
||||
|
||||
@@ -16,7 +16,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||
|
||||
const {
|
||||
@@ -190,7 +190,10 @@ export const JiraGetImportDetail: React.FC = observer(() => {
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => commandPaletteStore.toggleCreateProjectModal(true)}
|
||||
onClick={() => {
|
||||
setTrackElement("JIRA_IMPORT_DETAIL");
|
||||
commandPaletteStore.toggleCreateProjectModal(true)
|
||||
}}
|
||||
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-custom-text-200"
|
||||
>
|
||||
<Plus className="h-4 w-4 text-custom-text-200" />
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { mutate } from "swr";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { IssueAttachmentService } from "services/issue";
|
||||
// hooks
|
||||
@@ -10,8 +13,8 @@ import useToast from "hooks/use-toast";
|
||||
import { IIssueAttachment } from "types";
|
||||
// fetch-keys
|
||||
import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
const maxFileSize = 5 * 1024 * 1024; // 5 MB
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "constants/common";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
@@ -19,14 +22,20 @@ type Props = {
|
||||
|
||||
const issueAttachmentService = new IssueAttachmentService();
|
||||
|
||||
export const IssueAttachmentUpload: React.FC<Props> = ({ disabled = false }) => {
|
||||
export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
|
||||
const { disabled = false } = props;
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (!acceptedFiles[0] || !workspaceSlug) return;
|
||||
|
||||
@@ -70,13 +79,15 @@ export const IssueAttachmentUpload: React.FC<Props> = ({ disabled = false }) =>
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
||||
onDrop,
|
||||
maxSize: maxFileSize,
|
||||
maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
multiple: false,
|
||||
disabled: isLoading || disabled,
|
||||
});
|
||||
|
||||
const fileError =
|
||||
fileRejections.length > 0 ? `Invalid file type or size (max ${maxFileSize / 1024 / 1024} MB)` : null;
|
||||
fileRejections.length > 0
|
||||
? `Invalid file type or size (max ${envConfig?.file_size_limit ?? MAX_FILE_SIZE / 1024 / 1024} MB)`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -99,4 +110,4 @@ export const IssueAttachmentUpload: React.FC<Props> = ({ disabled = false }) =>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{data?.project_detail.identifier}-{data?.sequence_id}
|
||||
{data?.project_detail?.identifier}-{data?.sequence_id}
|
||||
</span>
|
||||
{""}? All of the data related to the issue will be permanently removed. This action cannot be
|
||||
undone.
|
||||
|
||||
@@ -112,7 +112,6 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
const user = userStore.currentUser;
|
||||
console.log("envConfig", envConfig);
|
||||
// hooks
|
||||
const editorSuggestion = useEditorSuggestions();
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -229,7 +228,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
...defaultValues,
|
||||
...initialData,
|
||||
});
|
||||
}, [setFocus, initialData, reset]);
|
||||
}, [setFocus, reset]);
|
||||
|
||||
// update projectId in form when projectId changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,14 +7,28 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { CalendarChart } from "components/issues";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { ICycleIssuesStore, IModuleIssuesStore, IProjectIssuesStore, IViewIssuesStore } from "store/issues";
|
||||
import { IIssueCalendarViewStore, IssueStore } from "store/issue";
|
||||
import {
|
||||
ICycleIssuesFilterStore,
|
||||
ICycleIssuesStore,
|
||||
IModuleIssuesFilterStore,
|
||||
IModuleIssuesStore,
|
||||
IProjectIssuesFilterStore,
|
||||
IProjectIssuesStore,
|
||||
IViewIssuesFilterStore,
|
||||
IViewIssuesStore,
|
||||
} from "store/issues";
|
||||
import { IIssueCalendarViewStore } from "store/issue";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IGroupedIssues } from "store/issues/types";
|
||||
|
||||
interface IBaseCalendarRoot {
|
||||
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
|
||||
issuesFilterStore:
|
||||
| IProjectIssuesFilterStore
|
||||
| IModuleIssuesFilterStore
|
||||
| ICycleIssuesFilterStore
|
||||
| IViewIssuesFilterStore;
|
||||
calendarViewStore: IIssueCalendarViewStore;
|
||||
QuickActions: FC<IQuickActionProps>;
|
||||
issueActions: {
|
||||
@@ -26,10 +40,9 @@ interface IBaseCalendarRoot {
|
||||
}
|
||||
|
||||
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
const { issueStore, calendarViewStore, QuickActions, issueActions, viewId } = props;
|
||||
const { projectIssuesFilter: issueFilterStore } = useMobxStore();
|
||||
const { issueStore, issuesFilterStore, calendarViewStore, QuickActions, issueActions, viewId } = props;
|
||||
|
||||
const displayFilters = issueFilterStore.issueFilters?.displayFilters;
|
||||
const displayFilters = issuesFilterStore.issueFilters?.displayFilters;
|
||||
|
||||
const issues = issueStore.getIssues;
|
||||
const groupedIssueIds = (issueStore.getIssuesIds ?? {}) as IGroupedIssues;
|
||||
@@ -75,7 +88,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
}
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE]
|
||||
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.UPDATE)
|
||||
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,11 @@ import { EIssueActions } from "../../types";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
|
||||
export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
const { cycleIssues: cycleIssueStore, cycleIssueCalendarView: cycleIssueCalendarViewStore } = useMobxStore();
|
||||
const {
|
||||
cycleIssues: cycleIssueStore,
|
||||
cycleIssuesFilter: cycleIssueFilterStore,
|
||||
cycleIssueCalendarView: cycleIssueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
|
||||
@@ -34,6 +38,7 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
return (
|
||||
<BaseCalendarRoot
|
||||
issueStore={cycleIssueStore}
|
||||
issuesFilterStore={cycleIssueFilterStore}
|
||||
calendarViewStore={cycleIssueCalendarViewStore}
|
||||
QuickActions={CycleIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
|
||||
@@ -10,7 +10,11 @@ import { EIssueActions } from "../../types";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
|
||||
export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
const { moduleIssues: moduleIssueStore, moduleIssueCalendarView: moduleIssueCalendarViewStore } = useMobxStore();
|
||||
const {
|
||||
moduleIssues: moduleIssueStore,
|
||||
moduleIssuesFilter: moduleIssueFilterStore,
|
||||
moduleIssueCalendarView: moduleIssueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string };
|
||||
@@ -33,6 +37,7 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
return (
|
||||
<BaseCalendarRoot
|
||||
issueStore={moduleIssueStore}
|
||||
issuesFilterStore={moduleIssueFilterStore}
|
||||
calendarViewStore={moduleIssueCalendarViewStore}
|
||||
QuickActions={ModuleIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
|
||||
@@ -15,25 +15,26 @@ export const CalendarLayout: React.FC = observer(() => {
|
||||
const {
|
||||
projectIssues: issueStore,
|
||||
issueCalendarView: issueCalendarViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
projectIssuesFilter: projectIssueFiltersStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
issueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseCalendarRoot
|
||||
issueStore={issueStore}
|
||||
issuesFilterStore={projectIssueFiltersStore}
|
||||
calendarViewStore={issueCalendarViewStore}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
|
||||
import { ProjectIssueQuickActions } from "components/issues";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../../types";
|
||||
@@ -14,7 +12,7 @@ import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
||||
const {
|
||||
viewIssues: projectViewIssuesStore,
|
||||
issueDetail: issueDetailStore,
|
||||
viewIssuesFilter: projectIssueViewFiltersStore,
|
||||
projectViewIssueCalendarView: projectViewIssueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
@@ -25,18 +23,19 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
projectViewIssuesStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
projectViewIssuesStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseCalendarRoot
|
||||
issueStore={projectViewIssuesStore}
|
||||
issuesFilterStore={projectIssueViewFiltersStore}
|
||||
calendarViewStore={projectViewIssueCalendarViewStore}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Button } from "@plane/ui";
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
// types
|
||||
import { ISearchIssueResponse } from "types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
@@ -26,7 +27,11 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
// states
|
||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||
|
||||
const { cycleIssue: cycleIssueStore, commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const {
|
||||
cycleIssue: cycleIssueStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
} = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -62,7 +67,10 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
primaryButton={{
|
||||
text: "New issue",
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => commandPaletteStore.toggleCreateIssueModal(true),
|
||||
onClick: () => {
|
||||
setTrackElement("CYCLE_EMPTY_STATE");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
},
|
||||
}}
|
||||
secondaryButton={
|
||||
<Button
|
||||
|
||||
@@ -15,7 +15,7 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { commandPalette: commandPaletteStore, project: projectStore } = useMobxStore();
|
||||
const { commandPalette: commandPaletteStore, project: projectStore, trackEvent: { setTrackElement } } = useMobxStore();
|
||||
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
|
||||
|
||||
@@ -29,7 +29,10 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
|
||||
primaryButton={{
|
||||
icon: <Plus className="h-4 w-4" />,
|
||||
text: "New Project",
|
||||
onClick: () => commandPaletteStore.toggleCreateProjectModal(true),
|
||||
onClick: () => {
|
||||
setTrackElement("ALL_ISSUES_EMPTY_STATE")
|
||||
commandPaletteStore.toggleCreateProjectModal(true)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -40,7 +43,10 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
|
||||
primaryButton={{
|
||||
text: "New issue",
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => commandPaletteStore.toggleCreateIssueModal(true),
|
||||
onClick: () => {
|
||||
setTrackElement("ALL_ISSUES_EMPTY_STATE")
|
||||
commandPaletteStore.toggleCreateIssueModal(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
// states
|
||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||
|
||||
const { moduleIssue: moduleIssueStore, commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const { moduleIssue: moduleIssueStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -58,7 +58,10 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
primaryButton={{
|
||||
text: "New issue",
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => commandPaletteStore.toggleCreateIssueModal(true),
|
||||
onClick: () => {
|
||||
setTrackElement("MODULE_EMPTY_STATE");
|
||||
commandPaletteStore.toggleCreateIssueModal(true)
|
||||
}
|
||||
}}
|
||||
secondaryButton={
|
||||
<Button
|
||||
|
||||
@@ -6,9 +6,17 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { EmptyState } from "components/common";
|
||||
// assets
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const ProjectViewEmptyState: React.FC = observer(() => {
|
||||
const { commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const router = useRouter();
|
||||
const { viewId } = router.query as { viewId: string };
|
||||
|
||||
const {
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
} = useMobxStore();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
@@ -19,7 +27,10 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
|
||||
primaryButton={{
|
||||
text: "New issue",
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => commandPaletteStore.toggleCreateIssueModal(true),
|
||||
onClick: () => {
|
||||
setTrackElement("VIEW_EMPTY_STATE");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,13 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { EmptyState } from "components/common";
|
||||
// assets
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export const ProjectEmptyState: React.FC = observer(() => {
|
||||
const { commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const {
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
} = useMobxStore();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
@@ -19,7 +23,10 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
primaryButton={{
|
||||
text: "New issue",
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => commandPaletteStore.toggleCreateIssueModal(true),
|
||||
onClick: () => {
|
||||
setTrackElement("PROJECT_EMPTY_STATE");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+12
-15
@@ -7,23 +7,24 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { AppliedFiltersList } from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
archivedIssueFilters: archivedIssueFiltersStore,
|
||||
projectArchivedIssuesFilter: { issueFilters, updateFilters },
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const userFilters = archivedIssueFiltersStore.userFilters;
|
||||
const userFilters = issueFilters?.filters;
|
||||
|
||||
// filters whose value not null or empty array
|
||||
const appliedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(userFilters).forEach(([key, value]) => {
|
||||
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
@@ -36,22 +37,18 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||
|
||||
// remove all values of the key if value is null
|
||||
if (!value) {
|
||||
archivedIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: null,
|
||||
},
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, {
|
||||
[key]: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = archivedIssueFiltersStore.userFilters?.[key] ?? [];
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
archivedIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: newValues,
|
||||
},
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, {
|
||||
[key]: newValues,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -59,12 +56,12 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters).forEach((key) => {
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
|
||||
archivedIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: { ...newFilters },
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, {
|
||||
...newFilters,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { AppliedFiltersList } from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
projectDraftIssuesFilter: { issueFilters, updateFilters },
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const userFilters = issueFilters?.filters;
|
||||
// filters whose value not null or empty array
|
||||
const appliedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
// remove all values of the key if value is null
|
||||
if (!value) {
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, {
|
||||
[key]: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, {
|
||||
[key]: newValues,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { ...newFilters });
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -4,3 +4,4 @@ export * from "./module-root";
|
||||
export * from "./project-view-root";
|
||||
export * from "./project-root";
|
||||
export * from "./archived-issue";
|
||||
export * from "./profile-issues-root";
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { AppliedFiltersList } from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query as {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
const {
|
||||
workspace: { workspaceLabels },
|
||||
workspaceProfileIssuesFilter: { issueFilters, updateFilters },
|
||||
projectMember: { projectMembers },
|
||||
} = useMobxStore();
|
||||
|
||||
const userFilters = issueFilters?.filters;
|
||||
|
||||
// filters whose value not null or empty array
|
||||
const appliedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug) return;
|
||||
if (!value) {
|
||||
updateFilters(workspaceSlug, EFilterType.FILTERS, { [key]: null });
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(workspaceSlug, EFilterType.FILTERS, {
|
||||
[key]: newValues,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
updateFilters(workspaceSlug, EFilterType.FILTERS, { ...newFilters });
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={workspaceLabels ?? []}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
states={[]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -57,10 +57,16 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
//Todo fix sort order in the structure
|
||||
issueStore.updateIssue(workspaceSlug, issue.project, issue.id, {
|
||||
start_date: payload.start_date,
|
||||
target_date: payload.target_date,
|
||||
});
|
||||
issueStore.updateIssue(
|
||||
workspaceSlug,
|
||||
issue.project,
|
||||
issue.id,
|
||||
{
|
||||
start_date: payload.start_date,
|
||||
target_date: payload.target_date,
|
||||
},
|
||||
viewId
|
||||
);
|
||||
};
|
||||
|
||||
const isAllowed = (projectDetails?.member_role || 0) >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FC, useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
@@ -9,7 +8,19 @@ import { Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { ICycleIssuesStore, IModuleIssuesStore, IProjectIssuesStore, IViewIssuesStore } from "store/issues";
|
||||
import {
|
||||
ICycleIssuesFilterStore,
|
||||
ICycleIssuesStore,
|
||||
IModuleIssuesFilterStore,
|
||||
IModuleIssuesStore,
|
||||
IProfileIssuesFilterStore,
|
||||
IProfileIssuesStore,
|
||||
IProjectDraftIssuesStore,
|
||||
IProjectIssuesFilterStore,
|
||||
IProjectIssuesStore,
|
||||
IViewIssuesFilterStore,
|
||||
IViewIssuesStore,
|
||||
} from "store/issues";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { IIssueKanBanViewStore } from "store/issue";
|
||||
// constants
|
||||
@@ -17,9 +28,22 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
//components
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IBaseKanBanLayout {
|
||||
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
|
||||
issueStore:
|
||||
| IProjectIssuesStore
|
||||
| IModuleIssuesStore
|
||||
| ICycleIssuesStore
|
||||
| IViewIssuesStore
|
||||
| IProjectDraftIssuesStore
|
||||
| IProfileIssuesStore;
|
||||
issuesFilterStore:
|
||||
| IProjectIssuesFilterStore
|
||||
| IModuleIssuesFilterStore
|
||||
| ICycleIssuesFilterStore
|
||||
| IViewIssuesFilterStore
|
||||
| IProfileIssuesFilterStore;
|
||||
kanbanViewStore: IIssueKanBanViewStore;
|
||||
QuickActions: FC<IQuickActionProps>;
|
||||
issueActions: {
|
||||
@@ -29,24 +53,33 @@ export interface IBaseKanBanLayout {
|
||||
};
|
||||
showLoader?: boolean;
|
||||
viewId?: string;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBaseKanBanLayout) => {
|
||||
const { issueStore, kanbanViewStore, QuickActions, issueActions, showLoader, viewId } = props;
|
||||
const {
|
||||
issueStore,
|
||||
issuesFilterStore,
|
||||
kanbanViewStore,
|
||||
QuickActions,
|
||||
issueActions,
|
||||
showLoader,
|
||||
viewId,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
project: { workspaceProjects },
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectIssuesFilter: issueFilterStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issues = issueStore?.getIssues || {};
|
||||
const issueIds = issueStore?.getIssuesIds || [];
|
||||
|
||||
const displayFilters = issueFilterStore?.issueFilters?.displayFilters;
|
||||
const displayProperties = issueFilterStore?.issueFilters?.displayProperties || null;
|
||||
const displayFilters = issuesFilterStore?.issueFilters?.displayFilters;
|
||||
const displayProperties = issuesFilterStore?.issueFilters?.displayProperties || null;
|
||||
|
||||
const sub_group_by: string | null = displayFilters?.sub_group_by || null;
|
||||
|
||||
@@ -60,6 +93,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
|
||||
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||
|
||||
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
|
||||
const onDragStart = () => {
|
||||
setIsDragStarted(true);
|
||||
};
|
||||
@@ -103,7 +137,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLoader && issueStore?.loader === "mutation" && (
|
||||
{showLoader && issueStore?.loader === "init-loader" && (
|
||||
<div className="fixed top-16 right-2 z-30 bg-custom-background-80 shadow-custom-shadow-sm w-10 h-10 rounded flex justify-center items-center">
|
||||
<Spinner className="w-5 h-5" />
|
||||
</div>
|
||||
@@ -144,11 +178,14 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={workspaceProjects}
|
||||
enableQuickIssueCreate
|
||||
enableQuickIssueCreate={enableQuickAdd}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
quickAddCallback={issueStore?.quickAddIssue}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={!enableIssueCreation}
|
||||
isReadOnly={!enableInlineEditing}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
@@ -185,6 +222,10 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
projects={workspaceProjects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={true}
|
||||
enableQuickIssueCreate={enableQuickAdd}
|
||||
isReadOnly={!enableInlineEditing}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
|
||||
@@ -17,6 +17,7 @@ interface IssueBlockProps {
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
@@ -30,6 +31,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
handleIssues,
|
||||
quickActions,
|
||||
displayProperties,
|
||||
isReadOnly,
|
||||
} = props;
|
||||
|
||||
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
|
||||
@@ -91,6 +93,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
handleIssues={updateIssue}
|
||||
displayProperties={displayProperties}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ interface IssueBlocksListProps {
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => {
|
||||
@@ -27,6 +28,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
|
||||
handleIssues,
|
||||
quickActions,
|
||||
displayProperties,
|
||||
isReadOnly,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -50,6 +52,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
|
||||
columnId={columnId}
|
||||
sub_group_id={sub_group_id}
|
||||
isDragDisabled={isDragDisabled}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getValueFromObject } from "constants/issue";
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IGroupByKanBan {
|
||||
issues: IIssueResponse;
|
||||
@@ -40,6 +41,9 @@ export interface IGroupByKanBan {
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
@@ -63,6 +67,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
isDragStarted,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
disableIssueCreation,
|
||||
isReadOnly,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const verticalAlignPosition = (_list: any) =>
|
||||
@@ -86,6 +93,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
issues_count={issueIds?.[getValueFromObject(_list, listKey) as string]?.length || 0}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -95,10 +104,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
verticalAlignPosition(_list) ? `w-[0px] overflow-hidden` : `w-full transition-all`
|
||||
}`}
|
||||
>
|
||||
<Droppable
|
||||
droppableId={`${getValueFromObject(_list, listKey) as string}__${sub_group_id}`}
|
||||
isDropDisabled={isDragDisabled}
|
||||
>
|
||||
<Droppable droppableId={`${getValueFromObject(_list, listKey) as string}__${sub_group_id}`}>
|
||||
{(provided: any, snapshot: any) => (
|
||||
<div
|
||||
className={`w-full h-full relative transition-all ${
|
||||
@@ -118,6 +124,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
) : (
|
||||
isDragDisabled && (
|
||||
@@ -149,7 +156,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isDragStarted && isDragDisabled && (
|
||||
{/* {isDragStarted && isDragDisabled && (
|
||||
<div className="invisible group-hover:visible transition-all text-sm absolute top-12 bottom-10 left-0 right-0 bg-custom-background-100/40 text-center">
|
||||
<div className="rounded inline-flex mt-80 h-8 px-3 justify-center items-center bg-custom-background-80 text-custom-text-100 font-medium">
|
||||
{`This board is ordered by "${replaceUnderscoreIfSnakeCase(
|
||||
@@ -157,7 +164,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
)}"`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -192,6 +199,9 @@ export interface IKanBan {
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
@@ -218,6 +228,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
isDragStarted,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
disableIssueCreation,
|
||||
isReadOnly,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const { issueKanBanView: issueKanBanViewStore } = useMobxStore();
|
||||
@@ -246,6 +259,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
isReadOnly={isReadOnly}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -271,6 +287,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
isReadOnly={isReadOnly}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -296,6 +315,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
isReadOnly={isReadOnly}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -321,6 +343,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
isReadOnly={isReadOnly}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -346,6 +371,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
isReadOnly={isReadOnly}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -371,6 +399,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
isReadOnly={isReadOnly}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -396,6 +427,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
isReadOnly={isReadOnly}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { HeaderGroupByCard } from "./group-by-card";
|
||||
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||
// ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IAssigneesHeader {
|
||||
column_id: string;
|
||||
@@ -15,6 +16,8 @@ export interface IAssigneesHeader {
|
||||
issues_count: number;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
export const Icon = ({ user }: any) => <Avatar name={user.display_name} src={user.avatar} size="base" />;
|
||||
@@ -29,6 +32,8 @@ export const AssigneesHeader: FC<IAssigneesHeader> = observer((props) => {
|
||||
issues_count,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const assignee = column_value ?? null;
|
||||
@@ -56,6 +61,8 @@ export const AssigneesHeader: FC<IAssigneesHeader> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
issuePayload={{ assignees: [assignee?.id] }}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { HeaderGroupByCard } from "./group-by-card";
|
||||
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||
import { Icon } from "./assignee";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface ICreatedByHeader {
|
||||
column_id: string;
|
||||
@@ -14,6 +15,8 @@ export interface ICreatedByHeader {
|
||||
issues_count: number;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
export const CreatedByHeader: FC<ICreatedByHeader> = observer((props) => {
|
||||
@@ -26,6 +29,8 @@ export const CreatedByHeader: FC<ICreatedByHeader> = observer((props) => {
|
||||
issues_count,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const createdBy = column_value ?? null;
|
||||
@@ -53,6 +58,8 @@ export const CreatedByHeader: FC<ICreatedByHeader> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
issuePayload={{ created_by: createdBy?.id }}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -15,6 +15,7 @@ import useToast from "hooks/use-toast";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import { IIssue, ISearchIssueResponse } from "types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
interface IHeaderGroupByCard {
|
||||
sub_group_by: string | null;
|
||||
@@ -26,13 +27,26 @@ interface IHeaderGroupByCard {
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
issuePayload: Partial<IIssue>;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
const moduleService = new ModuleService();
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
const { sub_group_by, column_id, icon, title, count, kanBanToggle, handleKanBanToggle, issuePayload } = props;
|
||||
const {
|
||||
sub_group_by,
|
||||
column_id,
|
||||
icon,
|
||||
title,
|
||||
count,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
issuePayload,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
} = props;
|
||||
const verticalAlignPosition = kanBanToggle?.groupByHeaderMinMax.includes(column_id);
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
@@ -84,7 +98,12 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateIssueModal isOpen={isOpen} handleClose={() => setIsOpen(false)} prePopulateData={issuePayload} />
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isOpen}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
prePopulateData={issuePayload}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
{renderExistingIssueModal && (
|
||||
<ExistingIssuesListModal
|
||||
isOpen={openExistingIssueListModal}
|
||||
@@ -126,30 +145,31 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderExistingIssueModal ? (
|
||||
<CustomMenu
|
||||
width="auto"
|
||||
customButton={
|
||||
<span className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all">
|
||||
<Plus height={14} width={14} strokeWidth={2} />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => setIsOpen(true)}>
|
||||
<span className="flex items-center justify-start gap-2">Create issue</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => setOpenExistingIssueListModal(true)}>
|
||||
<span className="flex items-center justify-start gap-2">Add an existing issue</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
) : (
|
||||
<div
|
||||
className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Plus width={14} strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
{!disableIssueCreation &&
|
||||
(renderExistingIssueModal ? (
|
||||
<CustomMenu
|
||||
width="auto"
|
||||
customButton={
|
||||
<span className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all">
|
||||
<Plus height={14} width={14} strokeWidth={2} />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => setIsOpen(true)}>
|
||||
<span className="flex items-center justify-start gap-2">Create issue</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => setOpenExistingIssueListModal(true)}>
|
||||
<span className="flex items-center justify-start gap-2">Add an existing issue</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
) : (
|
||||
<div
|
||||
className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Plus width={14} strokeWidth={2} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { LabelHeader } from "./label";
|
||||
import { CreatedByHeader } from "./created_by";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IKanBanGroupByHeaderRoot {
|
||||
column_id: string;
|
||||
@@ -17,10 +18,22 @@ export interface IKanBanGroupByHeaderRoot {
|
||||
issues_count: number;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = observer(
|
||||
({ column_id, column_value, sub_group_by, group_by, issues_count, kanBanToggle, handleKanBanToggle }) => (
|
||||
({
|
||||
column_id,
|
||||
column_value,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
issues_count,
|
||||
kanBanToggle,
|
||||
disableIssueCreation,
|
||||
handleKanBanToggle,
|
||||
currentStore,
|
||||
}) => (
|
||||
<>
|
||||
{group_by && group_by === "project" && (
|
||||
<ProjectHeader
|
||||
@@ -32,6 +45,8 @@ export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = obser
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -45,6 +60,8 @@ export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = obser
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{group_by && group_by === "state_detail.group" && (
|
||||
@@ -57,6 +74,8 @@ export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = obser
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{group_by && group_by === "priority" && (
|
||||
@@ -69,6 +88,8 @@ export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = obser
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{group_by && group_by === "labels" && (
|
||||
@@ -81,6 +102,8 @@ export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = obser
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{group_by && group_by === "assignees" && (
|
||||
@@ -93,6 +116,8 @@ export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = obser
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{group_by && group_by === "created_by" && (
|
||||
@@ -105,6 +130,8 @@ export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = obser
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { HeaderGroupByCard } from "./group-by-card";
|
||||
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface ILabelHeader {
|
||||
column_id: string;
|
||||
@@ -13,6 +14,8 @@ export interface ILabelHeader {
|
||||
issues_count: number;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
const Icon = ({ color }: any) => (
|
||||
@@ -29,6 +32,8 @@ export const LabelHeader: FC<ILabelHeader> = observer((props) => {
|
||||
issues_count,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const label = column_value ?? null;
|
||||
@@ -56,6 +61,8 @@ export const LabelHeader: FC<ILabelHeader> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
issuePayload={{ labels: [label?.id] }}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||
|
||||
// Icons
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IPriorityHeader {
|
||||
column_id: string;
|
||||
@@ -17,6 +18,8 @@ export interface IPriorityHeader {
|
||||
issues_count: number;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
export const PriorityHeader: FC<IPriorityHeader> = observer((props) => {
|
||||
@@ -29,6 +32,8 @@ export const PriorityHeader: FC<IPriorityHeader> = observer((props) => {
|
||||
issues_count,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const priority = column_value || null;
|
||||
@@ -56,6 +61,8 @@ export const PriorityHeader: FC<IPriorityHeader> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
issuePayload={{ priority: priority?.key }}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { HeaderGroupByCard } from "./group-by-card";
|
||||
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||
// emoji helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IProjectHeader {
|
||||
column_id: string;
|
||||
@@ -15,6 +16,8 @@ export interface IProjectHeader {
|
||||
issues_count: number;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
const Icon = ({ emoji }: any) => <div className="w-6 h-6">{renderEmoji(emoji)}</div>;
|
||||
@@ -29,6 +32,8 @@ export const ProjectHeader: FC<IProjectHeader> = observer((props) => {
|
||||
issues_count,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const project = column_value ?? null;
|
||||
@@ -56,6 +61,8 @@ export const ProjectHeader: FC<IProjectHeader> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
issuePayload={{ project: project?.id }}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { HeaderGroupByCard } from "./group-by-card";
|
||||
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IStateGroupHeader {
|
||||
column_id: string;
|
||||
@@ -14,6 +15,8 @@ export interface IStateGroupHeader {
|
||||
issues_count: number;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => (
|
||||
@@ -32,6 +35,8 @@ export const StateGroupHeader: FC<IStateGroupHeader> = observer((props) => {
|
||||
issues_count,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const stateGroup = column_value || null;
|
||||
@@ -59,6 +64,8 @@ export const StateGroupHeader: FC<IStateGroupHeader> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
issuePayload={{}}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { HeaderGroupByCard } from "./group-by-card";
|
||||
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||
import { Icon } from "./state-group";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IStateHeader {
|
||||
column_id: string;
|
||||
@@ -14,6 +15,8 @@ export interface IStateHeader {
|
||||
issues_count: number;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
export const StateHeader: FC<IStateHeader> = observer((props) => {
|
||||
@@ -26,6 +29,8 @@ export const StateHeader: FC<IStateHeader> = observer((props) => {
|
||||
issues_count,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
const state = column_value ?? null;
|
||||
@@ -53,6 +58,8 @@ export const StateHeader: FC<IStateHeader> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
issuePayload={{ state: state?.id }}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AssigneesHeader } from "./assignee";
|
||||
import { PriorityHeader } from "./priority";
|
||||
import { LabelHeader } from "./label";
|
||||
import { CreatedByHeader } from "./created_by";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IKanBanSubGroupByHeaderRoot {
|
||||
column_id: string;
|
||||
@@ -16,10 +17,22 @@ export interface IKanBanSubGroupByHeaderRoot {
|
||||
issues_count: number;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
|
||||
export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> = observer((props) => {
|
||||
const { column_id, column_value, sub_group_by, group_by, issues_count, kanBanToggle, handleKanBanToggle } = props;
|
||||
const {
|
||||
column_id,
|
||||
column_value,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
issues_count,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,6 +46,8 @@ export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> =
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{sub_group_by && sub_group_by === "state_detail.group" && (
|
||||
@@ -45,6 +60,8 @@ export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> =
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{sub_group_by && sub_group_by === "priority" && (
|
||||
@@ -57,6 +74,8 @@ export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> =
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{sub_group_by && sub_group_by === "labels" && (
|
||||
@@ -69,6 +88,8 @@ export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> =
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{sub_group_by && sub_group_by === "assignees" && (
|
||||
@@ -81,6 +102,8 @@ export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> =
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
{sub_group_by && sub_group_by === "created_by" && (
|
||||
@@ -93,6 +116,8 @@ export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> =
|
||||
issues_count={issues_count}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -19,10 +19,11 @@ export interface IKanBanProperties {
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
showEmptyGroup: boolean;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) => {
|
||||
const { sub_group_id, columnId: group_id, issue, handleIssues, displayProperties, showEmptyGroup } = props;
|
||||
const { sub_group_id, columnId: group_id, issue, handleIssues, displayProperties, isReadOnly } = props;
|
||||
|
||||
const handleState = (state: IState) => {
|
||||
handleIssues(
|
||||
@@ -89,7 +90,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.state || null}
|
||||
onChange={handleState}
|
||||
disabled={false}
|
||||
disabled={isReadOnly}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
)}
|
||||
@@ -99,7 +100,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
<IssuePropertyPriority
|
||||
value={issue?.priority || null}
|
||||
onChange={handlePriority}
|
||||
disabled={false}
|
||||
disabled={isReadOnly}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
)}
|
||||
@@ -110,7 +111,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.labels || null}
|
||||
onChange={handleLabel}
|
||||
disabled={false}
|
||||
disabled={isReadOnly}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
)}
|
||||
@@ -120,7 +121,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
<IssuePropertyDate
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
disabled={false}
|
||||
disabled={isReadOnly}
|
||||
placeHolder="Start date"
|
||||
/>
|
||||
)}
|
||||
@@ -130,7 +131,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
<IssuePropertyDate
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
disabled={false}
|
||||
disabled={isReadOnly}
|
||||
placeHolder="Target date"
|
||||
/>
|
||||
)}
|
||||
@@ -142,7 +143,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
value={issue?.assignees || null}
|
||||
hideDropdownArrow
|
||||
onChange={handleAssignee}
|
||||
disabled={false}
|
||||
disabled={isReadOnly}
|
||||
multiple
|
||||
/>
|
||||
)}
|
||||
@@ -153,7 +154,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.estimate_point || null}
|
||||
onChange={handleEstimate}
|
||||
disabled={false}
|
||||
disabled={isReadOnly}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { IIssue } from "types";
|
||||
import { EIssueActions } from "../../types";
|
||||
// components
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface ICycleKanBanLayout {}
|
||||
|
||||
@@ -18,7 +19,11 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
|
||||
|
||||
// store
|
||||
const { cycleIssues: cycleIssueStore, cycleIssueKanBanView: cycleIssueKanBanViewStore } = useMobxStore();
|
||||
const {
|
||||
cycleIssues: cycleIssueStore,
|
||||
cycleIssuesFilter: cycleIssueFilterStore,
|
||||
cycleIssueKanBanView: cycleIssueKanBanViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
@@ -39,10 +44,12 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issueStore={cycleIssueStore}
|
||||
issuesFilterStore={cycleIssueFilterStore}
|
||||
kanbanViewStore={cycleIssueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={CycleIssueQuickActions}
|
||||
viewId={cycleId}
|
||||
currentStore={EProjectStore.CYCLE}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { ProjectIssueQuickActions } from "components/issues";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
|
||||
export interface IKanBanLayout {}
|
||||
|
||||
export const DraftKanBanLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query as { workspaceSlug: string };
|
||||
|
||||
const {
|
||||
projectDraftIssues: issueStore,
|
||||
projectDraftIssuesFilter: projectIssuesFilterStore,
|
||||
issueKanBanView: issueKanBanViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issueStore.removeIssue(workspaceSlug, issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issuesFilterStore={projectIssuesFilterStore}
|
||||
issueStore={issueStore}
|
||||
kanbanViewStore={issueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { IIssue } from "types";
|
||||
// constants
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IModuleKanBanLayout {}
|
||||
|
||||
@@ -20,8 +21,8 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
// store
|
||||
const {
|
||||
moduleIssues: moduleIssueStore,
|
||||
moduleIssuesFilter: moduleIssueFilterStore,
|
||||
moduleIssueKanBanView: moduleIssueKanBanViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
|
||||
// const handleIssues = useCallback(
|
||||
@@ -58,17 +59,19 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
|
||||
moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, issue.id, moduleId, issue.bridge_id);
|
||||
moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id);
|
||||
},
|
||||
};
|
||||
return (
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issueStore={moduleIssueStore}
|
||||
issuesFilterStore={moduleIssueFilterStore}
|
||||
kanbanViewStore={moduleIssueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={ModuleIssueQuickActions}
|
||||
viewId={moduleId}
|
||||
currentStore={EProjectStore.MODULE}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,166 +1,48 @@
|
||||
import { FC, useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
import { ProjectIssueQuickActions } from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IProfileIssuesKanBanLayout {}
|
||||
export const ProfileIssuesKanBanLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string };
|
||||
|
||||
export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
const {
|
||||
workspace: workspaceStore,
|
||||
project: projectStore,
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
profileIssues: profileIssuesStore,
|
||||
profileIssueFilters: profileIssueFiltersStore,
|
||||
workspaceProfileIssues: profileIssuesStore,
|
||||
workspaceProfileIssuesFilter: profileIssueFiltersStore,
|
||||
issueKanBanView: issueKanBanViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
|
||||
const issues = profileIssuesStore?.getIssues;
|
||||
|
||||
const sub_group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.sub_group_by || null;
|
||||
|
||||
const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null;
|
||||
|
||||
const order_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.order_by || null;
|
||||
|
||||
const userDisplayFilters = profileIssueFiltersStore?.userDisplayFilters || null;
|
||||
|
||||
const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null;
|
||||
|
||||
const currentKanBanView: "swimlanes" | "default" = profileIssueFiltersStore?.userDisplayFilters?.sub_group_by
|
||||
? "swimlanes"
|
||||
: "default";
|
||||
|
||||
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||
|
||||
// const onDragStart = () => {
|
||||
// setIsDragStarted(true);
|
||||
// };
|
||||
|
||||
const onDragEnd = (result: any) => {
|
||||
setIsDragStarted(false);
|
||||
if (!result) return;
|
||||
|
||||
if (
|
||||
result.destination &&
|
||||
result.source &&
|
||||
result.destination.droppableId === result.source.droppableId &&
|
||||
result.destination.index === result.source.index
|
||||
)
|
||||
return;
|
||||
|
||||
currentKanBanView === "default"
|
||||
? issueKanBanViewStore?.handleDragDrop(result.source, result.destination)
|
||||
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (action === EIssueActions.UPDATE) {
|
||||
profileIssuesStore.updateIssueStructure(group_by, sub_group_by, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === EIssueActions.DELETE) profileIssuesStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
await profileIssuesStore.updateIssue(workspaceSlug, userId, issue.id, issue);
|
||||
},
|
||||
[profileIssuesStore, issueDetailStore, workspaceSlug]
|
||||
);
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
|
||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||
issueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||
await profileIssuesStore.removeIssue(workspaceSlug, userId, issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const labels = workspaceStore.workspaceLabels || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = projectStore?.workspaceProjects || null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{profileIssuesStore.loader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{currentKanBanView === "default" ? (
|
||||
<KanBan
|
||||
issues={{}}
|
||||
issueIds={[]}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
issues={{}}
|
||||
issueIds={[]}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issuesFilterStore={profileIssueFiltersStore}
|
||||
issueStore={profileIssuesStore}
|
||||
kanbanViewStore={issueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
currentStore={EProjectStore.PROFILE}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { IIssue } from "types";
|
||||
// constants
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IKanBanLayout {}
|
||||
|
||||
@@ -18,30 +19,32 @@ export const KanBanLayout: React.FC = observer(() => {
|
||||
|
||||
const {
|
||||
projectIssues: issueStore,
|
||||
projectIssuesFilter: issuesFilterStore,
|
||||
issueKanBanView: issueKanBanViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issueDetailStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
|
||||
await issueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issueDetailStore.deleteIssue(workspaceSlug, issue.project, issue.id);
|
||||
await issueStore.removeIssue(workspaceSlug, issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issuesFilterStore={issuesFilterStore}
|
||||
issueStore={issueStore}
|
||||
kanbanViewStore={issueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
currentStore={EProjectStore.PROJECT}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { EIssueActions } from "../../types";
|
||||
import { ProjectIssueQuickActions } from "../../quick-action-dropdowns";
|
||||
// components
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
export interface IViewKanBanLayout {}
|
||||
|
||||
@@ -18,30 +19,32 @@ export const ProjectViewKanBanLayout: React.FC = observer(() => {
|
||||
|
||||
const {
|
||||
viewIssues: projectViewIssuesStore,
|
||||
viewIssuesFilter: projectIssueViewFiltersStore,
|
||||
issueKanBanView: projectViewIssueKanBanViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issueDetailStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
|
||||
await projectViewIssuesStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issueDetailStore.deleteIssue(workspaceSlug, issue.project, issue.id);
|
||||
await projectViewIssuesStore.removeIssue(workspaceSlug, issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issuesFilterStore={projectIssueViewFiltersStore}
|
||||
issueStore={projectViewIssuesStore}
|
||||
kanbanViewStore={projectViewIssueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
currentStore={EProjectStore.PROJECT_VIEW}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } f
|
||||
// constants
|
||||
import { getValueFromObject } from "constants/issue";
|
||||
import { EIssueActions } from "../types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
|
||||
interface ISubGroupSwimlaneHeader {
|
||||
issues: IIssueResponse;
|
||||
@@ -20,9 +21,10 @@ interface ISubGroupSwimlaneHeader {
|
||||
listKey: string;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
}
|
||||
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
issues,
|
||||
issueIds,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
@@ -30,6 +32,8 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
listKey,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
disableIssueCreation,
|
||||
currentStore,
|
||||
}) => {
|
||||
const calculateIssueCount = (column_id: string) => {
|
||||
let issueCount = 0;
|
||||
@@ -54,6 +58,8 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -78,6 +84,10 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
isDragStarted?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
enableQuickIssueCreate: boolean;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
const {
|
||||
@@ -101,6 +111,9 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
members,
|
||||
projects,
|
||||
isDragStarted,
|
||||
disableIssueCreation,
|
||||
enableQuickIssueCreate,
|
||||
isReadOnly,
|
||||
} = props;
|
||||
|
||||
const calculateIssueCount = (column_id: string) => {
|
||||
@@ -128,6 +141,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full border-b border-custom-border-400 border-dashed" />
|
||||
@@ -153,8 +167,9 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
enableQuickIssueCreate
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isDragStarted={isDragStarted}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -183,6 +198,10 @@ export interface IKanBanSwimLanes {
|
||||
members: IUserLite[] | null;
|
||||
projects: IProject[] | null;
|
||||
isDragStarted?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
currentStore?: EProjectStore;
|
||||
enableQuickIssueCreate: boolean;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
@@ -205,6 +224,10 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
members,
|
||||
projects,
|
||||
isDragStarted,
|
||||
disableIssueCreation,
|
||||
enableQuickIssueCreate,
|
||||
isReadOnly,
|
||||
currentStore,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -220,6 +243,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`id`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -233,6 +258,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`id`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -246,6 +273,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`key`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -259,6 +288,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`key`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -272,6 +302,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`id`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -285,6 +317,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`id`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -298,6 +332,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
listKey={`id`}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
currentStore={currentStore}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -324,6 +360,9 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
members={members}
|
||||
projects={projects}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -349,6 +388,9 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
members={members}
|
||||
projects={projects}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -374,6 +416,9 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
members={members}
|
||||
projects={projects}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -399,6 +444,9 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
members={members}
|
||||
projects={projects}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -424,6 +472,9 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
members={members}
|
||||
projects={projects}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -449,6 +500,9 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
members={members}
|
||||
projects={projects}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -474,6 +528,9 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
members={members}
|
||||
projects={projects}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -499,6 +556,9 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
members={members}
|
||||
projects={projects}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user