Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d488a62ac | |||
| 568a69b185 | |||
| 045f6ccdcb | |||
| 768b23aed1 | |||
| 27e82659b7 | |||
| 01d96b2027 | |||
| c54838b791 | |||
| d953a50014 | |||
| 9e6b7b10ec | |||
| 6790b613a6 | |||
| 891af46240 | |||
| 8c70657106 | |||
| ffaa931bdf | |||
| 9bea238e04 | |||
| 2625b685f4 | |||
| eabe5917d5 | |||
| da42cd7835 | |||
| 866d995af7 | |||
| 0bf49cf389 | |||
| 19bc48b553 | |||
| ff5811b7a2 | |||
| af042719fe | |||
| a7eab16d39 | |||
| dbd15312f1 | |||
| 8c38e42800 | |||
| 4e2fe590fa | |||
| 0bbfe95cc7 | |||
| 9a30a07cf5 | |||
| b6e47ccdae | |||
| 4280c4d1b1 | |||
| b1c78fe4c8 |
+2
-2
@@ -115,5 +115,5 @@ scripts/
|
||||
# i18n auto-generated types (regenerated on every build)
|
||||
packages/i18n/src/types/keys.generated.ts
|
||||
|
||||
# Local security advisory notes (not for version control)
|
||||
/advisories.md
|
||||
# Local security notes (not for version control)
|
||||
/security/
|
||||
|
||||
@@ -19,6 +19,7 @@ from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from plane.settings.storage import S3Storage
|
||||
from plane.utils.path_validator import sanitize_filename
|
||||
from plane.db.models import FileAsset, User, Workspace
|
||||
from plane.app.permissions import WorkspaceUserPermission
|
||||
from plane.api.views.base import BaseAPIView
|
||||
from plane.api.serializers import (
|
||||
UserAssetUploadSerializer,
|
||||
@@ -404,6 +405,12 @@ class UserServerAssetEndpoint(BaseAPIView):
|
||||
class GenericAssetEndpoint(BaseAPIView):
|
||||
"""This endpoint is used to upload generic assets that can be later bound to entities."""
|
||||
|
||||
# The workspace is taken straight from the URL slug, so every method must
|
||||
# verify the caller is an active member of that workspace. Without this the
|
||||
# endpoint is a cross-workspace IDOR (the public-API sibling of the
|
||||
# CVE-2026-46558 dashboard fix).
|
||||
permission_classes = [WorkspaceUserPermission]
|
||||
|
||||
use_read_replica = True
|
||||
|
||||
@asset_docs(
|
||||
|
||||
@@ -22,6 +22,27 @@ from plane.db.models import User
|
||||
class MagicCodeProvider(CredentialAdapter):
|
||||
provider = "magic-code"
|
||||
|
||||
# Max wrong-code verification attempts per issued token before the token
|
||||
# is invalidated. Prevents brute-forcing the 6-digit code space within
|
||||
# the token TTL window.
|
||||
MAX_VERIFY_ATTEMPTS = 5
|
||||
|
||||
# Atomic INCR + first-time EXPIRE for the verify-attempt counter.
|
||||
# Using a dedicated counter key with this script makes the increment
|
||||
# safe under concurrent wrong-code requests; a plain JSON read/modify/
|
||||
# write would race and let parallel attackers exceed the cap.
|
||||
_INCREMENT_VERIFY_ATTEMPTS_SCRIPT = (
|
||||
'local count = redis.call("INCR", KEYS[1]) '
|
||||
'if count == 1 then '
|
||||
' redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1])) '
|
||||
'end '
|
||||
'return count'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _verify_attempts_key(token_key):
|
||||
return f"{token_key}:verify_attempts"
|
||||
|
||||
def __init__(self, request, key, code=None, callback=None):
|
||||
(EMAIL_HOST, ENABLE_MAGIC_LINK_LOGIN) = get_configuration_value(
|
||||
[
|
||||
@@ -92,6 +113,9 @@ class MagicCodeProvider(CredentialAdapter):
|
||||
expiry = 600
|
||||
|
||||
ri.set(key, json.dumps(value), ex=expiry)
|
||||
# Reset the verify-attempt counter so each newly issued token starts
|
||||
# with a fresh budget of MAX_VERIFY_ATTEMPTS.
|
||||
ri.delete(self._verify_attempts_key(key))
|
||||
return key, token
|
||||
|
||||
def set_user_data(self):
|
||||
@@ -114,12 +138,52 @@ class MagicCodeProvider(CredentialAdapter):
|
||||
},
|
||||
}
|
||||
)
|
||||
# Delete the token from redis if the code match is successful
|
||||
# Delete the token and its counter from redis on success.
|
||||
ri.delete(self.key)
|
||||
ri.delete(self._verify_attempts_key(self.key))
|
||||
return
|
||||
else:
|
||||
email = str(self.key).replace("magic_", "", 1)
|
||||
if User.objects.filter(email=email).exists():
|
||||
user_exists = User.objects.filter(email=email).exists()
|
||||
|
||||
# Atomically increment the verify-attempt counter in Redis.
|
||||
# The Lua script sets the TTL only on the first increment so
|
||||
# the lockout window matches the remaining token TTL and does
|
||||
# not get extended by every wrong-code attempt.
|
||||
# ri.ttl() returns -2 (missing), -1 (no expiry), 0 (sub-second
|
||||
# remaining; Redis floors to whole seconds), or a positive int.
|
||||
# Clamp to >=1 because EXPIRE key 0 immediately deletes the key
|
||||
# and would let an attacker bypass the cap in the final second.
|
||||
remaining_ttl = ri.ttl(self.key)
|
||||
if remaining_ttl is None or remaining_ttl <= 0:
|
||||
remaining_ttl = 1
|
||||
verify_attempts = int(
|
||||
ri.eval(
|
||||
self._INCREMENT_VERIFY_ATTEMPTS_SCRIPT,
|
||||
1,
|
||||
self._verify_attempts_key(self.key),
|
||||
remaining_ttl,
|
||||
)
|
||||
)
|
||||
|
||||
if verify_attempts >= self.MAX_VERIFY_ATTEMPTS:
|
||||
# Invalidate the token (and counter) so further attempts
|
||||
# must regenerate; regeneration is itself attempt-counted.
|
||||
ri.delete(self.key)
|
||||
ri.delete(self._verify_attempts_key(self.key))
|
||||
if user_exists:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN"],
|
||||
error_message="EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP"],
|
||||
error_message="EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP",
|
||||
payload={"email": str(email)},
|
||||
)
|
||||
|
||||
if user_exists:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_MAGIC_CODE_SIGN_IN"],
|
||||
error_message="INVALID_MAGIC_CODE_SIGN_IN",
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
|
||||
from rest_framework import status
|
||||
@@ -15,7 +18,9 @@ from plane.authentication.adapter.error import (
|
||||
|
||||
|
||||
class AuthenticationThrottle(AnonRateThrottle):
|
||||
rate = "30/minute"
|
||||
# Rate is configurable per-deployment via the AUTHENTICATION_RATE_LIMIT
|
||||
# env var (DRF format: "<num>/<period>" where period is second/minute/hour/day).
|
||||
rate = os.environ.get("AUTHENTICATION_RATE_LIMIT", "10/minute")
|
||||
scope = "authentication"
|
||||
|
||||
def throttle_failure_view(self, request, *args, **kwargs):
|
||||
@@ -28,6 +33,22 @@ class AuthenticationThrottle(AnonRateThrottle):
|
||||
return Response(e.get_error_dict(), status=status.HTTP_429_TOO_MANY_REQUESTS)
|
||||
|
||||
|
||||
def authentication_throttle_allows(request):
|
||||
"""
|
||||
Apply AuthenticationThrottle to a plain django.views.View request.
|
||||
|
||||
DRF's throttle_classes only run inside APIView.initial(); the magic
|
||||
sign-in / sign-up endpoints extend django.views.View to return
|
||||
HttpResponseRedirect from a form POST flow, so they need a manual
|
||||
throttle check. Returns True if the request is allowed through,
|
||||
False if it should be rejected with a RATE_LIMIT_EXCEEDED error.
|
||||
"""
|
||||
throttle = AuthenticationThrottle()
|
||||
# SimpleRateThrottle.allow_request only reads request.META and
|
||||
# request.user, both available on a plain Django HttpRequest.
|
||||
return throttle.allow_request(request, None)
|
||||
|
||||
|
||||
class EmailVerificationThrottle(UserRateThrottle):
|
||||
"""
|
||||
Throttle for email verification code generation.
|
||||
|
||||
@@ -26,7 +26,10 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||
from plane.authentication.rate_limit import (
|
||||
AuthenticationThrottle,
|
||||
authentication_throttle_allows,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url
|
||||
|
||||
|
||||
@@ -65,6 +68,18 @@ class MagicSignInEndpoint(View):
|
||||
email = request.POST.get("email", "").strip().lower()
|
||||
next_path = request.POST.get("next_path")
|
||||
|
||||
if not authentication_throttle_allows(request):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
|
||||
error_message="RATE_LIMIT_EXCEEDED",
|
||||
)
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=exc.get_error_dict(),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if code == "" or email == "":
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED"],
|
||||
@@ -136,6 +151,18 @@ class MagicSignUpEndpoint(View):
|
||||
email = request.POST.get("email", "").strip().lower()
|
||||
next_path = request.POST.get("next_path")
|
||||
|
||||
if not authentication_throttle_allows(request):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
|
||||
error_message="RATE_LIMIT_EXCEEDED",
|
||||
)
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_app=True),
|
||||
next_path=next_path,
|
||||
params=exc.get_error_dict(),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if code == "" or email == "":
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED"],
|
||||
|
||||
@@ -25,12 +25,18 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.authentication.rate_limit import (
|
||||
AuthenticationThrottle,
|
||||
authentication_throttle_allows,
|
||||
)
|
||||
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path, get_allowed_hosts
|
||||
|
||||
|
||||
class MagicGenerateSpaceEndpoint(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
throttle_classes = [AuthenticationThrottle]
|
||||
|
||||
def post(self, request):
|
||||
# Check if instance is configured
|
||||
instance = Instance.objects.first()
|
||||
@@ -60,6 +66,18 @@ class MagicSignInSpaceEndpoint(View):
|
||||
email = request.POST.get("email", "").strip().lower()
|
||||
next_path = request.POST.get("next_path")
|
||||
|
||||
if not authentication_throttle_allows(request):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
|
||||
error_message="RATE_LIMIT_EXCEEDED",
|
||||
)
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True),
|
||||
next_path=next_path,
|
||||
params=exc.get_error_dict(),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if code == "" or email == "":
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED"],
|
||||
@@ -119,6 +137,18 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
email = request.POST.get("email", "").strip().lower()
|
||||
next_path = request.POST.get("next_path")
|
||||
|
||||
if not authentication_throttle_allows(request):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
|
||||
error_message="RATE_LIMIT_EXCEEDED",
|
||||
)
|
||||
url = get_safe_redirect_url(
|
||||
base_url=base_host(request=request, is_space=True),
|
||||
next_path=next_path,
|
||||
params=exc.get_error_dict(),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if code == "" or email == "":
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED"],
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
"""Contract tests for the public REST API ``GenericAssetEndpoint``.
|
||||
|
||||
Regression coverage for the cross-workspace asset IDOR (the unfixed
|
||||
external-API sibling of CVE-2026-46558 / GHSA-qw87-v5w3-6vxx). The endpoint
|
||||
must reject any caller that is not an active member of the workspace named in
|
||||
the URL slug, regardless of the workspace their Personal Access Token came
|
||||
from.
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
|
||||
from plane.db.models import FileAsset, User, Workspace, WorkspaceMember
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def victim_user(db):
|
||||
"""A user that owns a separate workspace the attacker is not part of."""
|
||||
unique_id = uuid4().hex[:8]
|
||||
user = User.objects.create(
|
||||
email=f"victim-{unique_id}@plane.so",
|
||||
username=f"victim_{unique_id}",
|
||||
first_name="Victim",
|
||||
last_name="User",
|
||||
)
|
||||
user.set_password("test-password")
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def victim_workspace(db, victim_user):
|
||||
"""A workspace whose only active member is ``victim_user``.
|
||||
|
||||
The attacker (``create_user``, who authenticates ``api_key_client``) is
|
||||
deliberately NOT a member here.
|
||||
"""
|
||||
workspace = Workspace.objects.create(
|
||||
name="Victim Workspace",
|
||||
owner=victim_user,
|
||||
slug="victim-workspace",
|
||||
)
|
||||
WorkspaceMember.objects.create(workspace=workspace, member=victim_user, role=20)
|
||||
return workspace
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def victim_asset(db, victim_workspace, victim_user):
|
||||
"""An uploaded attachment that lives inside the victim workspace.
|
||||
|
||||
``storage_metadata`` is pre-populated so the PATCH handler does not enqueue
|
||||
the metadata Celery task during the test.
|
||||
"""
|
||||
return FileAsset.objects.create(
|
||||
attributes={"name": "secret.pdf", "type": "application/pdf", "size": 1024},
|
||||
asset=f"{victim_workspace.id}/secret.pdf",
|
||||
size=1024,
|
||||
workspace=victim_workspace,
|
||||
created_by=victim_user,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
is_uploaded=True,
|
||||
storage_metadata={"size": 1024},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
class TestGenericAssetCrossWorkspaceIDOR:
|
||||
"""A PAT holder must not reach assets in a workspace they don't belong to."""
|
||||
|
||||
def detail_url(self, slug, asset_id):
|
||||
return f"/api/v1/workspaces/{slug}/assets/{asset_id}/"
|
||||
|
||||
def list_url(self, slug):
|
||||
return f"/api/v1/workspaces/{slug}/assets/"
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_cross_workspace_asset_returns_403(self, api_key_client, victim_workspace, victim_asset):
|
||||
"""GET on another workspace's asset must be forbidden, not return a
|
||||
presigned download URL."""
|
||||
url = self.detail_url(victim_workspace.slug, victim_asset.id)
|
||||
|
||||
with mock.patch("plane.api.views.asset.S3Storage") as mock_storage:
|
||||
mock_storage.return_value.generate_presigned_url.return_value = "https://signed.example/download"
|
||||
response = api_key_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN, f"Got {response.status_code}: {response.data!r}"
|
||||
# The S3 download URL must never be minted for a non-member.
|
||||
mock_storage.return_value.generate_presigned_url.assert_not_called()
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_post_cross_workspace_asset_returns_403(self, api_key_client, victim_workspace):
|
||||
"""POST (upload) into another workspace must be forbidden and must not
|
||||
plant an asset row in the victim workspace."""
|
||||
url = self.list_url(victim_workspace.slug)
|
||||
payload = {"name": "evil.pdf", "type": "application/pdf", "size": 1024}
|
||||
|
||||
with mock.patch("plane.api.views.asset.S3Storage") as mock_storage:
|
||||
mock_storage.return_value.generate_presigned_post.return_value = {"url": "x", "fields": {}}
|
||||
response = api_key_client.post(url, payload, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN, f"Got {response.status_code}: {response.data!r}"
|
||||
assert FileAsset.objects.filter(workspace=victim_workspace).count() == 0
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_patch_cross_workspace_asset_returns_403(self, api_key_client, victim_workspace, victim_asset):
|
||||
"""PATCH on another workspace's asset must be forbidden and must leave
|
||||
the asset untouched."""
|
||||
url = self.detail_url(victim_workspace.slug, victim_asset.id)
|
||||
|
||||
response = api_key_client.patch(url, {"is_uploaded": False}, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN, f"Got {response.status_code}: {response.data!r}"
|
||||
victim_asset.refresh_from_db()
|
||||
assert victim_asset.is_uploaded is True
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_member_can_patch_own_workspace_asset(self, api_key_client, workspace, create_user):
|
||||
"""Positive control: an active member of the workspace can still update
|
||||
their own asset, so the fix does not over-block legitimate callers."""
|
||||
asset = FileAsset.objects.create(
|
||||
attributes={"name": "mine.pdf", "type": "application/pdf", "size": 10},
|
||||
asset=f"{workspace.id}/mine.pdf",
|
||||
size=10,
|
||||
workspace=workspace,
|
||||
created_by=create_user,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
is_uploaded=False,
|
||||
storage_metadata={"size": 10},
|
||||
)
|
||||
url = self.detail_url(workspace.slug, asset.id)
|
||||
|
||||
response = api_key_client.patch(url, {"is_uploaded": True}, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT, f"Got {response.status_code}: {response.data!r}"
|
||||
asset.refresh_from_db()
|
||||
assert asset.is_uploaded is True
|
||||
@@ -5,6 +5,7 @@
|
||||
import json
|
||||
import uuid
|
||||
import pytest
|
||||
from django.core.cache import cache
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
@@ -12,6 +13,8 @@ from django.test import Client
|
||||
from django.core.exceptions import ValidationError
|
||||
from unittest.mock import patch
|
||||
|
||||
from plane.authentication.provider.credentials.magic_code import MagicCodeProvider
|
||||
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||
from plane.db.models import User
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.license.models import Instance
|
||||
@@ -428,3 +431,198 @@ class TestMagicSignUp:
|
||||
|
||||
# Check if user is authenticated
|
||||
assert "_auth_user_id" in django_client.session
|
||||
|
||||
|
||||
def _generate_magic_token(api_client, email):
|
||||
"""Hit /magic-generate/ for `email` and return the token that landed in Redis."""
|
||||
gen_url = reverse("magic-generate")
|
||||
response = api_client.post(gen_url, {"email": email}, format="json")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
ri = redis_instance()
|
||||
return json.loads(ri.get(f"magic_{email}"))["token"]
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
class TestMagicSignInVerifyAttempts:
|
||||
"""Per-token wrong-code attempt counter and exhaustion behavior (GHSA-9pvm-fcf6-9234)."""
|
||||
|
||||
EMAIL = "verify-attempts@plane.so"
|
||||
|
||||
@pytest.fixture
|
||||
def setup_user(self, db):
|
||||
user = User.objects.create(email=self.EMAIL)
|
||||
user.set_password("user@123")
|
||||
user.save()
|
||||
return user
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_state(self):
|
||||
"""Reset throttle cache and magic-link redis state between tests in this class."""
|
||||
cache.clear()
|
||||
ri = redis_instance()
|
||||
ri.delete(f"magic_{self.EMAIL}")
|
||||
ri.delete(f"magic_{self.EMAIL}:verify_attempts")
|
||||
yield
|
||||
cache.clear()
|
||||
ri.delete(f"magic_{self.EMAIL}")
|
||||
ri.delete(f"magic_{self.EMAIL}:verify_attempts")
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("plane.bgtasks.magic_link_code_task.magic_link.delay")
|
||||
def test_exhausted_after_max_wrong_attempts(
|
||||
self, mock_magic_link, django_client, api_client, setup_user, setup_instance
|
||||
):
|
||||
"""
|
||||
After MAX_VERIFY_ATTEMPTS wrong codes the next verify must redirect with
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN and both Redis keys must be gone.
|
||||
|
||||
With MAX_VERIFY_ATTEMPTS=5 the 5th wrong attempt itself triggers exhaustion
|
||||
(4 INVALID + 1 EXHAUSTED), matching the >= check in set_user_data.
|
||||
"""
|
||||
_generate_magic_token(api_client, self.EMAIL)
|
||||
url = reverse("magic-sign-in")
|
||||
ri = redis_instance()
|
||||
|
||||
# First (MAX-1) wrong attempts: each redirects with INVALID_MAGIC_CODE_SIGN_IN.
|
||||
for i in range(MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 1):
|
||||
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
|
||||
assert response.status_code == 302, f"attempt {i+1} unexpected status"
|
||||
assert "INVALID_MAGIC_CODE_SIGN_IN" in response.url, f"attempt {i+1} did not return INVALID"
|
||||
|
||||
# Token and counter both still live, with counter at MAX-1.
|
||||
assert ri.exists(f"magic_{self.EMAIL}")
|
||||
assert int(ri.get(f"magic_{self.EMAIL}:verify_attempts")) == MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 1
|
||||
|
||||
# The MAX-th wrong attempt is the exhausting one.
|
||||
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
|
||||
assert response.status_code == 302
|
||||
assert "EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN" in response.url
|
||||
|
||||
# Both the token and the counter must be deleted.
|
||||
assert not ri.exists(f"magic_{self.EMAIL}")
|
||||
assert not ri.exists(f"magic_{self.EMAIL}:verify_attempts")
|
||||
|
||||
# Follow-up verify now sees the key as missing and reports EXPIRED.
|
||||
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
|
||||
assert response.status_code == 302
|
||||
assert "EXPIRED_MAGIC_CODE_SIGN_IN" in response.url
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("plane.bgtasks.magic_link_code_task.magic_link.delay")
|
||||
def test_counter_increments_on_each_wrong_attempt(
|
||||
self, mock_magic_link, django_client, api_client, setup_user, setup_instance
|
||||
):
|
||||
"""The verify_attempts counter increments by exactly one per wrong-code POST."""
|
||||
_generate_magic_token(api_client, self.EMAIL)
|
||||
url = reverse("magic-sign-in")
|
||||
ri = redis_instance()
|
||||
counter_key = f"magic_{self.EMAIL}:verify_attempts"
|
||||
|
||||
# Before any wrong attempt the counter does not exist (Lua INCR creates it).
|
||||
assert not ri.exists(counter_key)
|
||||
|
||||
for expected in range(1, MagicCodeProvider.MAX_VERIFY_ATTEMPTS):
|
||||
django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
|
||||
assert int(ri.get(counter_key)) == expected, f"counter mismatch after {expected} attempts"
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("plane.bgtasks.magic_link_code_task.magic_link.delay")
|
||||
def test_counter_resets_on_token_regeneration(
|
||||
self, mock_magic_link, django_client, api_client, setup_user, setup_instance
|
||||
):
|
||||
"""
|
||||
Regenerating the magic-link must reset the verify-attempt counter so the
|
||||
user isn't pre-locked-out by a previous session's wrong attempts.
|
||||
"""
|
||||
_generate_magic_token(api_client, self.EMAIL)
|
||||
url = reverse("magic-sign-in")
|
||||
ri = redis_instance()
|
||||
counter_key = f"magic_{self.EMAIL}:verify_attempts"
|
||||
|
||||
for _ in range(MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 2):
|
||||
django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
|
||||
assert int(ri.get(counter_key)) == MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 2
|
||||
|
||||
# Regenerate the magic-link — the counter should be cleared.
|
||||
_generate_magic_token(api_client, self.EMAIL)
|
||||
assert not ri.exists(counter_key)
|
||||
|
||||
# Fresh wrong attempt now produces INVALID (not EXHAUSTED) and counter starts at 1.
|
||||
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
|
||||
assert "INVALID_MAGIC_CODE_SIGN_IN" in response.url
|
||||
assert int(ri.get(counter_key)) == 1
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
class TestMagicSignUpVerifyAttempts:
|
||||
"""Sign-up flow gets the same per-token attempt cap (no existing User row)."""
|
||||
|
||||
EMAIL = "signup-verify-attempts@plane.so"
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_state(self):
|
||||
cache.clear()
|
||||
ri = redis_instance()
|
||||
ri.delete(f"magic_{self.EMAIL}")
|
||||
ri.delete(f"magic_{self.EMAIL}:verify_attempts")
|
||||
yield
|
||||
cache.clear()
|
||||
ri.delete(f"magic_{self.EMAIL}")
|
||||
ri.delete(f"magic_{self.EMAIL}:verify_attempts")
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("plane.bgtasks.magic_link_code_task.magic_link.delay")
|
||||
def test_signup_exhausted_after_max_wrong_attempts(
|
||||
self, mock_magic_link, django_client, api_client, setup_instance
|
||||
):
|
||||
"""The MAX-th wrong code on the sign-up endpoint returns the SIGN_UP variant of EXHAUSTED."""
|
||||
_generate_magic_token(api_client, self.EMAIL)
|
||||
url = reverse("magic-sign-up")
|
||||
ri = redis_instance()
|
||||
|
||||
for _ in range(MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 1):
|
||||
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
|
||||
assert "INVALID_MAGIC_CODE_SIGN_UP" in response.url
|
||||
|
||||
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
|
||||
assert "EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP" in response.url
|
||||
assert not ri.exists(f"magic_{self.EMAIL}")
|
||||
assert not ri.exists(f"magic_{self.EMAIL}:verify_attempts")
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
class TestAuthenticationThrottle:
|
||||
"""Per-IP throttle on the redirect-flow magic-link endpoints."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_state(self):
|
||||
cache.clear()
|
||||
yield
|
||||
cache.clear()
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_magic_sign_in_throttled(self, django_client, setup_instance):
|
||||
"""Posting past the configured rate from one IP returns RATE_LIMIT_EXCEEDED."""
|
||||
url = reverse("magic-sign-in")
|
||||
# Drop the rate so the test doesn't have to fire 10+ requests.
|
||||
with patch.object(AuthenticationThrottle, "rate", "2/minute"):
|
||||
for _ in range(2):
|
||||
response = django_client.post(url, {"email": "throttle@plane.so", "code": "000000"}, follow=False)
|
||||
assert response.status_code == 302
|
||||
assert "RATE_LIMIT_EXCEEDED" not in response.url
|
||||
|
||||
# The 3rd request from the same IP within the window trips the throttle.
|
||||
response = django_client.post(url, {"email": "throttle@plane.so", "code": "000000"}, follow=False)
|
||||
assert response.status_code == 302
|
||||
assert "RATE_LIMIT_EXCEEDED" in response.url
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_magic_sign_up_throttled(self, django_client, setup_instance):
|
||||
"""The sign-up sibling shares the same scope and trips on the same per-IP budget."""
|
||||
url = reverse("magic-sign-up")
|
||||
with patch.object(AuthenticationThrottle, "rate", "1/minute"):
|
||||
response = django_client.post(url, {"email": "throttle-up@plane.so", "code": "000000"}, follow=False)
|
||||
assert "RATE_LIMIT_EXCEEDED" not in response.url
|
||||
|
||||
response = django_client.post(url, {"email": "throttle-up@plane.so", "code": "000000"}, follow=False)
|
||||
assert "RATE_LIMIT_EXCEEDED" in response.url
|
||||
|
||||
@@ -27,7 +27,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// layouts
|
||||
import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
|
||||
// plane web imports
|
||||
import { useWorkItemProperties } from "@/plane-web/hooks/use-issue-properties";
|
||||
import { useWorkItemProperties } from "@/hooks/use-issue-properties";
|
||||
import { WorkItemDetailRoot } from "@/plane-web/components/browse/workItem-detail";
|
||||
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
@@ -22,7 +22,7 @@ import { SidebarProjectsListItem } from "@/components/workspace/sidebar/projects
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import type { TProject } from "@/plane-web/types";
|
||||
import type { TProject } from "@plane/types";
|
||||
import { ExtendedSidebarWrapper } from "./extended-sidebar-wrapper";
|
||||
|
||||
export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar() {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AppHeader } from "@/components/core/app-header";
|
||||
import { ContentWrapper } from "@/components/core/content-wrapper";
|
||||
import { ProfileSidebar } from "@/components/profile/sidebar";
|
||||
// constants
|
||||
import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys";
|
||||
import { USER_PROFILE_PROJECT_SEGREGATION } from "@plane/constants";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
|
||||
@@ -18,7 +18,7 @@ import { ProfileStateDistribution } from "@/components/profile/overview/state-di
|
||||
import { ProfileStats } from "@/components/profile/overview/stats";
|
||||
import { ProfileWorkload } from "@/components/profile/overview/workload";
|
||||
// constants
|
||||
import { USER_PROFILE_DATA } from "@/constants/fetch-keys";
|
||||
import { USER_PROFILE_DATA } from "@plane/constants";
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { Breadcrumbs, Header } from "@plane/ui";
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { IssueDetailQuickActions } from "@/components/issues/issue-detail/issue-detail-quick-actions";
|
||||
// constants
|
||||
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
|
||||
import { ISSUE_DETAILS } from "@plane/constants";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// plane web
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import { useEditorAsset } from "@/hooks/store/use-editor-asset";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/hooks/store";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
// services
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web imports
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages";
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/hooks/store";
|
||||
|
||||
export interface IPagesHeaderProps {
|
||||
showButton?: boolean;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import useSWR from "swr";
|
||||
import { AppHeader } from "@/components/core/app-header";
|
||||
import { ContentWrapper } from "@/components/core/content-wrapper";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
|
||||
import { EPageStoreType, usePageStore } from "@/hooks/store";
|
||||
// local components
|
||||
import type { Route } from "./+types/layout";
|
||||
import { PageDetailsHeader } from "./header";
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// plane web imports
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
|
||||
import { EPageStoreType, usePageStore } from "@/hooks/store";
|
||||
|
||||
export const PagesListHeader = observer(function PagesListHeader() {
|
||||
// states
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web hooks
|
||||
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { EPageStoreType } from "@/hooks/store";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
const getPageType = (pageType?: string | null): TPageNavigationTabs => {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { SingleIntegrationCard } from "@/components/integration/single-integrati
|
||||
import { IntegrationAndImportExportBanner } from "@/components/ui/integration-and-import-export-banner";
|
||||
import { IntegrationsSettingsLoader } from "@/components/ui/loader/settings/integration";
|
||||
// constants
|
||||
import { APP_INTEGRATIONS } from "@/constants/fetch-keys";
|
||||
import { APP_INTEGRATIONS } from "@plane/constants";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Outlet } from "react-router";
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
import { WorkspaceContentWrapper } from "@/plane-web/components/workspace/content-wrapper";
|
||||
import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail";
|
||||
import { AppRailVisibilityProvider } from "@/lib/app-rail";
|
||||
import { GlobalModals } from "@/plane-web/components/common/modal/global";
|
||||
import { WorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper";
|
||||
import type { Route } from "./+types/layout";
|
||||
|
||||
@@ -24,7 +24,7 @@ import emptyInvitation from "@/app/assets/empty-state/invitation.svg?url";
|
||||
// components
|
||||
import { EmptyState } from "@/components/common/empty-state";
|
||||
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
||||
import { USER_WORKSPACES_LIST } from "@plane/constants";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUser, useUserProfile } from "@/hooks/store/user";
|
||||
|
||||
@@ -11,7 +11,7 @@ import useSWR from "swr";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { OnboardingRoot } from "@/components/onboarding";
|
||||
// constants
|
||||
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
||||
import { USER_WORKSPACES_LIST } from "@plane/constants";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
|
||||
@@ -13,7 +13,7 @@ import { CheckIcon, CloseIcon } from "@plane/propel/icons";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { EmptySpace, EmptySpaceItem } from "@/components/ui/empty-space";
|
||||
// constants
|
||||
import { WORKSPACE_INVITATION } from "@/constants/fetch-keys";
|
||||
import { WORKSPACE_INVITATION } from "@plane/constants";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SwitcherLabel } from "@/components/common/switcher-label";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import type { TProject } from "@/plane-web/types";
|
||||
import type { TProject } from "@plane/types";
|
||||
|
||||
type TProjectBreadcrumbProps = {
|
||||
workspaceSlug: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { CreateUpdateProjectViewModal } from "@/components/views/modal";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
// plane web hooks
|
||||
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { EPageStoreType } from "@/hooks/store";
|
||||
|
||||
export type TProjectLevelModalsProps = {
|
||||
workspaceSlug: string;
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
import { RichTextEditor } from "@/components/editor/rich-text";
|
||||
// plane web constants
|
||||
import { AI_EDITOR_TASKS, LOADING_TEXTS } from "@/constants/ai";
|
||||
import { AI_EDITOR_TASKS, LOADING_TEXTS } from "@plane/constants";
|
||||
// plane web services
|
||||
import type { TTaskPayload } from "@/services/ai.service";
|
||||
import { AIService } from "@/services/ai.service";
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
// store
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
export type TPageHeaderExtraActionsProps = {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { getCoverImageType, uploadCoverImage } from "@/helpers/cover-image.helpe
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web types
|
||||
import type { TProject } from "@/plane-web/types/projects";
|
||||
import type { TProject } from "@plane/types";
|
||||
import { ProjectAttributes } from "./attributes";
|
||||
import { getProjectFormValues } from "./utils";
|
||||
|
||||
@@ -110,7 +110,7 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
|
||||
if (setToFavorite) {
|
||||
handleAddToFavorites(res.id);
|
||||
}
|
||||
handleNextStep(res.id);
|
||||
return handleNextStep(res.id);
|
||||
})
|
||||
.catch((err) => {
|
||||
try {
|
||||
@@ -119,8 +119,9 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
|
||||
|
||||
const nameError = errorData.name?.includes("PROJECT_NAME_ALREADY_EXIST");
|
||||
const identifierError = errorData?.identifier?.includes("PROJECT_IDENTIFIER_ALREADY_EXIST");
|
||||
const nameSpecialCharError = errorData?.name?.includes("PROJECT_NAME_CANNOT_CONTAIN_SPECIAL_CHARACTERS");
|
||||
|
||||
if (nameError || identifierError) {
|
||||
if (nameError || identifierError || nameSpecialCharError) {
|
||||
if (nameError) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
@@ -136,6 +137,14 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
|
||||
message: t("project_identifier_already_taken"),
|
||||
});
|
||||
}
|
||||
|
||||
if (nameSpecialCharError) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("toast.error"),
|
||||
message: t("project_name_cannot_contain_special_characters"),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { CircleDot, XCircle } from "lucide-react";
|
||||
import { RelatedIcon, DuplicatePropertyIcon } from "@plane/propel/icons";
|
||||
import type { TRelationObject } from "@/components/issues/issue-detail-widgets/relations";
|
||||
import type { TIssueRelationTypes } from "../../types";
|
||||
import type { TIssueRelationTypes } from "@plane/types";
|
||||
|
||||
export * from "./activity";
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { EProductSubscriptionEnum } from "@plane/types";
|
||||
import { getSubscriptionName } from "@plane/utils";
|
||||
// components
|
||||
import { DiscountInfo } from "@/components/license/modal/card/discount-info";
|
||||
import type { TPlanDetail } from "@/constants/plans";
|
||||
import type { TPlanDetail } from "@/components/workspace/billing/comparison/plans";
|
||||
// local imports
|
||||
import { PlanFrequencyToggle } from "./frequency-toggle";
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import { observer } from "mobx-react";
|
||||
import type { EProductSubscriptionEnum, TBillingFrequency } from "@plane/types";
|
||||
// components
|
||||
import { PlansComparisonBase, shouldRenderPlanDetail } from "@/components/workspace/billing/comparison/base";
|
||||
import type { TPlanePlans } from "@/constants/plans";
|
||||
import { PLANE_PLANS } from "@/constants/plans";
|
||||
import type { TPlanePlans } from "@/components/workspace/billing/comparison/plans";
|
||||
import { PLANE_PLANS } from "@/components/workspace/billing/comparison/plans";
|
||||
// plane web imports
|
||||
import { PlanDetail } from "./plan-detail";
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { AppRailVisibilityProvider as CoreProvider } from "@/lib/app-rail";
|
||||
|
||||
interface AppRailVisibilityProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* CE AppRailVisibilityProvider
|
||||
* Wraps core provider with isEnabled hardcoded to false
|
||||
*/
|
||||
export const AppRailVisibilityProvider = observer(function AppRailVisibilityProvider({
|
||||
children,
|
||||
}: AppRailVisibilityProviderProps) {
|
||||
return <CoreProvider isEnabled={false}>{children}</CoreProvider>;
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
// editor
|
||||
import type { TEmbedConfig } from "@plane/editor";
|
||||
// plane types
|
||||
import type { TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
|
||||
// plane web components
|
||||
import { IssueEmbedUpgradeCard } from "@/plane-web/components/pages";
|
||||
|
||||
export type TIssueEmbedHookProps = {
|
||||
fetchEmbedSuggestions?: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>;
|
||||
projectId?: string;
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const useIssueEmbed = (props: TIssueEmbedHookProps) => {
|
||||
const widgetCallback = () => <IssueEmbedUpgradeCard />;
|
||||
|
||||
const issueEmbedProps: TEmbedConfig["issue"] = {
|
||||
widgetCallback,
|
||||
};
|
||||
|
||||
return {
|
||||
issueEmbedProps,
|
||||
};
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
// types
|
||||
import type { TTimelineTypeCore } from "@plane/types";
|
||||
import { GANTT_TIMELINE_TYPE } from "@plane/types";
|
||||
// Plane-web
|
||||
|
||||
import type { IBaseTimelineStore } from "@/plane-web/store/timeline/base-timeline.store";
|
||||
import type { ITimelineStore } from "../store/timeline";
|
||||
|
||||
export const getTimelineStore = (
|
||||
timelineStore: ITimelineStore,
|
||||
timelineType: TTimelineTypeCore
|
||||
): IBaseTimelineStore => {
|
||||
if (timelineType === GANTT_TIMELINE_TYPE.ISSUE) {
|
||||
return timelineStore.issuesTimeLineStore as IBaseTimelineStore;
|
||||
}
|
||||
if (timelineType === GANTT_TIMELINE_TYPE.MODULE) {
|
||||
return timelineStore.modulesTimeLineStore as IBaseTimelineStore;
|
||||
}
|
||||
if (timelineType === GANTT_TIMELINE_TYPE.PROJECT) {
|
||||
return timelineStore.projectTimeLineStore;
|
||||
}
|
||||
if (timelineType === GANTT_TIMELINE_TYPE.GROUPED) {
|
||||
return timelineStore.groupedTimeLineStore;
|
||||
}
|
||||
throw new Error(`Unknown timeline type: ${timelineType}`);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export type TIssueRelationTypes = "blocking" | "blocked_by" | "duplicate" | "relates_to";
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./projects";
|
||||
export * from "./issue-types";
|
||||
export * from "./gantt-chart";
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./issue-property-values.d";
|
||||
@@ -1,2 +0,0 @@
|
||||
export type TIssuePropertyValues = object;
|
||||
export type TIssuePropertyValueErrors = object;
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
// CE re-exports the core navigation pane extension types directly
|
||||
// EE overrides this with specific extension data types
|
||||
export type {
|
||||
INavigationPaneExtension,
|
||||
INavigationPaneExtensionComponent,
|
||||
INavigationPaneExtensionProps,
|
||||
} from "@/components/pages/navigation-pane";
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./projects";
|
||||
export * from "./project-activity";
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { IPartialProject, IProject } from "@plane/types";
|
||||
|
||||
export type TPartialProject = IPartialProject;
|
||||
|
||||
export type TProject = TPartialProject & IProject;
|
||||
@@ -14,7 +14,7 @@ import type { IApiToken } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore } from "@plane/ui";
|
||||
// fetch-keys
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
import { API_TOKENS_LIST } from "@plane/constants";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { IApiToken } from "@plane/types";
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
import { renderFormattedDate, csvDownload } from "@plane/utils";
|
||||
// constants
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
import { API_TOKENS_LIST } from "@plane/constants";
|
||||
// local imports
|
||||
import { CreateApiTokenForm } from "./form";
|
||||
import { GeneratedTokenDetails } from "./generated-token-details";
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import type { TProjectActivity } from "@/plane-web/types";
|
||||
import type { TProjectActivity } from "@plane/types";
|
||||
import { ActivityBlockComponent } from "./activity-block";
|
||||
import { iconsMap, messages } from "./helper";
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
StatePropertyIcon,
|
||||
} from "@plane/propel/icons";
|
||||
import { store } from "@/lib/store-context";
|
||||
import type { TProjectActivity } from "@/plane-web/types";
|
||||
import type { TProjectActivity } from "@plane/types";
|
||||
|
||||
type ActivityIconMap = {
|
||||
[key: string]: FC<{ className?: string }>;
|
||||
|
||||
@@ -11,7 +11,7 @@ import useSWR from "swr";
|
||||
import type { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// constants
|
||||
import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys";
|
||||
import { CYCLE_ISSUES_WITH_PARAMS } from "@plane/constants";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
|
||||
@@ -19,7 +19,7 @@ import { cn, sortBySelectedFirst } from "@plane/utils";
|
||||
// hooks
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// plane web imports
|
||||
import type { TProject } from "@/plane-web/types";
|
||||
import type { TProject } from "@plane/types";
|
||||
// local imports
|
||||
import { DropdownButton } from "../buttons";
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useEditorFlagging } from "@/hooks/use-editor-flagging";
|
||||
// local imports
|
||||
import { EditorMentionsRoot } from "../embeds/mentions";
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useEditorFlagging } from "@/hooks/use-editor-flagging";
|
||||
// plane web service
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { LiteToolbar } from "./lite-toolbar";
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import React from "react";
|
||||
import { ArrowUp, Paperclip } from "lucide-react";
|
||||
// constants
|
||||
import type { ToolbarMenuItem } from "@/constants/editor";
|
||||
import { IMAGE_ITEM } from "@/constants/editor";
|
||||
import type { ToolbarMenuItem } from "@plane/editor";
|
||||
import { IMAGE_ITEM } from "@plane/editor";
|
||||
|
||||
type LiteToolbarProps = {
|
||||
onSubmit: (e: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>) => void;
|
||||
|
||||
@@ -19,8 +19,8 @@ import type { ISvgIcons } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
// constants
|
||||
import { cn } from "@plane/utils";
|
||||
import type { ToolbarMenuItem } from "@/constants/editor";
|
||||
import { TOOLBAR_ITEMS } from "@/constants/editor";
|
||||
import type { ToolbarMenuItem } from "@plane/editor";
|
||||
import { TOOLBAR_ITEMS } from "@plane/editor";
|
||||
// helpers
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { PageProps } from "@react-pdf/renderer";
|
||||
import { Document, Font, Page } from "@react-pdf/renderer";
|
||||
import type { PageProps, Styles } from "@react-pdf/renderer";
|
||||
import { Document, Font, Page, StyleSheet } from "@react-pdf/renderer";
|
||||
import { Html } from "react-pdf-html";
|
||||
// assets
|
||||
import interBold from "@/app/assets/fonts/inter/bold.ttf?url";
|
||||
@@ -17,8 +17,177 @@ import interSemibold from "@/app/assets/fonts/inter/semibold.ttf?url";
|
||||
import interThin from "@/app/assets/fonts/inter/thin.ttf?url";
|
||||
import interUltraBold from "@/app/assets/fonts/inter/ultrabold.ttf?url";
|
||||
import interUltraLight from "@/app/assets/fonts/inter/ultralight.ttf?url";
|
||||
// constants
|
||||
import { EDITOR_PDF_DOCUMENT_STYLESHEET } from "@/constants/editor";
|
||||
// plane imports
|
||||
import { convertRemToPixel } from "@plane/utils";
|
||||
|
||||
const EDITOR_PDF_FONT_FAMILY_STYLES: Styles = {
|
||||
"*:not(.courier, .courier-bold)": { fontFamily: "Inter" },
|
||||
".courier": { fontFamily: "Courier" },
|
||||
".courier-bold": { fontFamily: "Courier-Bold" },
|
||||
};
|
||||
|
||||
const EDITOR_PDF_TYPOGRAPHY_STYLES: Styles = {
|
||||
// page title
|
||||
"h1.page-title": {
|
||||
fontSize: convertRemToPixel(1.6),
|
||||
fontWeight: "bold",
|
||||
marginTop: 0,
|
||||
marginBottom: convertRemToPixel(2),
|
||||
},
|
||||
// headings
|
||||
"h1:not(.page-title)": {
|
||||
fontSize: convertRemToPixel(1.4),
|
||||
fontWeight: "semibold",
|
||||
marginTop: convertRemToPixel(2),
|
||||
marginBottom: convertRemToPixel(0.25),
|
||||
},
|
||||
h2: {
|
||||
fontSize: convertRemToPixel(1.2),
|
||||
fontWeight: "semibold",
|
||||
marginTop: convertRemToPixel(1.4),
|
||||
marginBottom: convertRemToPixel(0.0625),
|
||||
},
|
||||
h3: {
|
||||
fontSize: convertRemToPixel(1.1),
|
||||
fontWeight: "semibold",
|
||||
marginTop: convertRemToPixel(1),
|
||||
marginBottom: convertRemToPixel(0.0625),
|
||||
},
|
||||
h4: {
|
||||
fontSize: convertRemToPixel(1),
|
||||
fontWeight: "semibold",
|
||||
marginTop: convertRemToPixel(1),
|
||||
marginBottom: convertRemToPixel(0.0625),
|
||||
},
|
||||
h5: {
|
||||
fontSize: convertRemToPixel(0.9),
|
||||
fontWeight: "semibold",
|
||||
marginTop: convertRemToPixel(1),
|
||||
marginBottom: convertRemToPixel(0.0625),
|
||||
},
|
||||
h6: {
|
||||
fontSize: convertRemToPixel(0.8),
|
||||
fontWeight: "semibold",
|
||||
marginTop: convertRemToPixel(1),
|
||||
marginBottom: convertRemToPixel(0.0625),
|
||||
},
|
||||
// paragraph
|
||||
"p:not(table p)": {
|
||||
fontSize: convertRemToPixel(0.8),
|
||||
},
|
||||
"p:not(ol p, ul p)": {
|
||||
marginTop: convertRemToPixel(0.25),
|
||||
marginBottom: convertRemToPixel(0.0625),
|
||||
},
|
||||
};
|
||||
|
||||
const EDITOR_PDF_LIST_STYLES: Styles = {
|
||||
"ul, ol": {
|
||||
fontSize: convertRemToPixel(0.8),
|
||||
marginHorizontal: -20,
|
||||
},
|
||||
"ol p, ul p": {
|
||||
marginVertical: 0,
|
||||
},
|
||||
"ol li, ul li": {
|
||||
marginTop: convertRemToPixel(0.45),
|
||||
},
|
||||
"ul ul, ul ol, ol ol, ol ul": {
|
||||
marginVertical: 0,
|
||||
},
|
||||
"ul[data-type='taskList']": {
|
||||
position: "relative",
|
||||
},
|
||||
"div.input-checkbox": {
|
||||
position: "absolute",
|
||||
top: convertRemToPixel(0.15),
|
||||
left: -convertRemToPixel(1.2),
|
||||
height: convertRemToPixel(0.75),
|
||||
width: convertRemToPixel(0.75),
|
||||
borderWidth: "1.5px",
|
||||
borderStyle: "solid",
|
||||
borderRadius: convertRemToPixel(0.125),
|
||||
},
|
||||
"div.input-checkbox:not(.checked)": {
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#171717",
|
||||
},
|
||||
"div.input-checkbox.checked": {
|
||||
backgroundColor: "#3f76ff",
|
||||
borderColor: "#3f76ff",
|
||||
},
|
||||
"ul li[data-checked='true'] p": {
|
||||
color: "#a3a3a3",
|
||||
},
|
||||
};
|
||||
|
||||
const EDITOR_PDF_CODE_STYLES: Styles = {
|
||||
// code block
|
||||
"[data-node-type='code-block']": {
|
||||
marginVertical: convertRemToPixel(0.5),
|
||||
padding: convertRemToPixel(1),
|
||||
borderRadius: convertRemToPixel(0.5),
|
||||
backgroundColor: "#f7f7f7",
|
||||
fontSize: convertRemToPixel(0.7),
|
||||
},
|
||||
// inline code block
|
||||
"[data-node-type='inline-code-block']": {
|
||||
margin: 0,
|
||||
paddingVertical: convertRemToPixel(0.25 / 4 + 0.25 / 8),
|
||||
paddingHorizontal: convertRemToPixel(0.375),
|
||||
border: "0.5px solid #e5e5e5",
|
||||
borderRadius: convertRemToPixel(0.25),
|
||||
backgroundColor: "#e8e8e8",
|
||||
color: "#f97316",
|
||||
fontSize: convertRemToPixel(0.7),
|
||||
},
|
||||
};
|
||||
|
||||
const EDITOR_PDF_DOCUMENT_STYLESHEET = StyleSheet.create({
|
||||
...EDITOR_PDF_FONT_FAMILY_STYLES,
|
||||
...EDITOR_PDF_TYPOGRAPHY_STYLES,
|
||||
...EDITOR_PDF_LIST_STYLES,
|
||||
...EDITOR_PDF_CODE_STYLES,
|
||||
// quote block
|
||||
blockquote: {
|
||||
borderLeft: "3px solid gray",
|
||||
paddingLeft: convertRemToPixel(1),
|
||||
marginTop: convertRemToPixel(0.625),
|
||||
marginBottom: 0,
|
||||
marginHorizontal: 0,
|
||||
},
|
||||
img: {
|
||||
marginVertical: 0,
|
||||
borderRadius: convertRemToPixel(0.375),
|
||||
},
|
||||
// divider
|
||||
"div[data-type='horizontalRule']": {
|
||||
marginVertical: convertRemToPixel(1),
|
||||
height: 1,
|
||||
width: "100%",
|
||||
backgroundColor: "gray",
|
||||
},
|
||||
// mention block
|
||||
"[data-node-type='mention-block']": {
|
||||
margin: 0,
|
||||
color: "#3f76ff",
|
||||
backgroundColor: "#3f76ff33",
|
||||
paddingHorizontal: convertRemToPixel(0.375),
|
||||
},
|
||||
// table
|
||||
table: {
|
||||
marginTop: convertRemToPixel(0.5),
|
||||
marginBottom: convertRemToPixel(1),
|
||||
marginHorizontal: 0,
|
||||
},
|
||||
"table td": {
|
||||
padding: convertRemToPixel(0.625),
|
||||
border: "1px solid #e5e5e5",
|
||||
},
|
||||
"table p": {
|
||||
fontSize: convertRemToPixel(0.7),
|
||||
},
|
||||
});
|
||||
|
||||
Font.register({
|
||||
family: "Inter",
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useEditorFlagging } from "@/hooks/use-editor-flagging";
|
||||
|
||||
type RichTextEditorWrapperProps = MakeOptional<
|
||||
Omit<IRichTextEditorProps, "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { cn } from "@plane/utils";
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useEditorFlagging } from "@/hooks/use-editor-flagging";
|
||||
import { StickyEditorToolbar } from "./toolbar";
|
||||
|
||||
interface StickyEditorWrapperProps extends Omit<
|
||||
|
||||
@@ -15,8 +15,8 @@ import { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { TSticky } from "@plane/types";
|
||||
// constants
|
||||
import { cn } from "@plane/utils";
|
||||
import type { ToolbarMenuItem } from "@/constants/editor";
|
||||
import { TOOLBAR_ITEMS } from "@/constants/editor";
|
||||
import type { ToolbarMenuItem } from "@plane/editor";
|
||||
import { TOOLBAR_ITEMS } from "@plane/editor";
|
||||
// helpers
|
||||
import { ColorPalette } from "./color-palette";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { observer } from "mobx-react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { mutate } from "swr";
|
||||
// constants
|
||||
import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys";
|
||||
import { EXPORT_SERVICES_LIST } from "@plane/constants";
|
||||
// local imports
|
||||
import { ExportForm } from "./export-form";
|
||||
import { PrevExports } from "./prev-exports";
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Table } from "@plane/ui";
|
||||
// components
|
||||
import { ImportExportSettingsLoader } from "@/components/ui/loader/settings/import-and-export";
|
||||
// constants
|
||||
import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys";
|
||||
import { EXPORT_SERVICES_LIST } from "@plane/constants";
|
||||
// services
|
||||
import { IntegrationService } from "@/services/integrations";
|
||||
// local imports
|
||||
|
||||
@@ -32,7 +32,7 @@ import { GanttChartRowList } from "@/plane-web/components/gantt-chart/blocks/blo
|
||||
import { GanttChartBlocksList } from "@/plane-web/components/gantt-chart/blocks/blocks-list";
|
||||
import { IssueBulkOperationsRoot } from "@/plane-web/components/issues/bulk-operations";
|
||||
// plane web hooks
|
||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||
import { useBulkOperationStatus } from "@/hooks/use-bulk-operation-status";
|
||||
//
|
||||
import { DEFAULT_BLOCK_WIDTH, GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
|
||||
import { getItemPositionWidth } from "../views";
|
||||
|
||||
@@ -31,7 +31,7 @@ import { useUser } from "@/hooks/store/user";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// store types
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe/duplicate-popover";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
import { useDebouncedDuplicateIssues } from "@/hooks/use-debounced-duplicate-issues";
|
||||
// services
|
||||
import { IntakeWorkItemVersionService } from "@/services/inbox";
|
||||
// stores
|
||||
|
||||
@@ -26,7 +26,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web imports
|
||||
import { DeDupeButtonRoot } from "@/plane-web/components/de-dupe/de-dupe-button";
|
||||
import { DuplicateModalRoot } from "@/plane-web/components/de-dupe/duplicate-modal";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
import { useDebouncedDuplicateIssues } from "@/hooks/use-debounced-duplicate-issues";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
// local imports
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Loader } from "@plane/ui";
|
||||
import GithubLogo from "@/app/assets/services/github.png?url";
|
||||
import SlackLogo from "@/app/assets/services/slack.png?url";
|
||||
// constants
|
||||
import { WORKSPACE_INTEGRATIONS } from "@/constants/fetch-keys";
|
||||
import { WORKSPACE_INTEGRATIONS } from "@plane/constants";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { IWorkspaceIntegration, ISlackIntegration } from "@plane/types";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// fetch-keys
|
||||
import { SLACK_CHANNEL_INFO } from "@/constants/fetch-keys";
|
||||
import { SLACK_CHANNEL_INFO } from "@plane/constants";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import useIntegrationPopup from "@/hooks/use-integration-popup";
|
||||
|
||||
@@ -16,7 +16,7 @@ import { EIssueServiceType } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// plane web hooks
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
import { useFileSize } from "@/hooks/use-file-size";
|
||||
// types
|
||||
import type { TAttachmentHelpers } from "../issue-detail-widgets/attachments/helper";
|
||||
// components
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
// plane web hooks
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
import { useFileSize } from "@/hooks/use-file-size";
|
||||
// types
|
||||
import type { TAttachmentOperations } from "../issue-detail-widgets/attachments/helper";
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { EIssueLayoutTypes, EIssuesStoreType } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
// plane web imports
|
||||
import type { TProject } from "@/plane-web/types";
|
||||
import type { TProject } from "@plane/types";
|
||||
// local imports
|
||||
import { WorkItemsModal } from "../analytics/work-items/modal";
|
||||
import { WorkItemFiltersToggle } from "../work-item-filters/filters-toggle";
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import type { TIssueServiceType } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// plane web hooks
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
import { useFileSize } from "@/hooks/use-file-size";
|
||||
// local imports
|
||||
import { useAttachmentOperations } from "./helper";
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// Plane-web
|
||||
import { CreateUpdateEpicModal } from "@/plane-web/components/epics/epic-modal";
|
||||
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
import type { TIssueRelationTypes } from "@/plane-web/types";
|
||||
import type { TIssueRelationTypes } from "@plane/types";
|
||||
// helper
|
||||
import { DeleteIssueModal } from "../../delete-issue-modal";
|
||||
import { RelationIssueList } from "../../relations/issue-list";
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import { CustomMenu } from "@plane/ui";
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// Plane-web
|
||||
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
import type { TIssueRelationTypes } from "@/plane-web/types";
|
||||
import type { TIssueRelationTypes } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { observer } from "mobx-react";
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// Plane-web
|
||||
import { getRelationActivityContent, useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
import type { TIssueRelationTypes } from "@/plane-web/types";
|
||||
import type { TIssueRelationTypes } from "@plane/types";
|
||||
//
|
||||
import { IssueActivityBlockComponent } from "./";
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import useSize from "@/hooks/use-window-size";
|
||||
// plane web components
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe/duplicate-popover";
|
||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues/issue-details/issue-type-switcher";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
import { useDebouncedDuplicateIssues } from "@/hooks/use-debounced-duplicate-issues";
|
||||
// services
|
||||
import { WorkItemVersionService } from "@/services/issue";
|
||||
// local imports
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useProject } from "@/hooks/store/use-project";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// Plane web imports
|
||||
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
import type { TIssueRelationTypes } from "@/plane-web/types";
|
||||
import type { TIssueRelationTypes } from "@plane/types";
|
||||
import type { TRelationObject } from "../issue-detail-widgets/relations";
|
||||
|
||||
type TIssueRelationSelect = {
|
||||
|
||||
@@ -24,7 +24,7 @@ import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { renderFormattedPayloadDate, cn } from "@plane/utils";
|
||||
// constants
|
||||
import { MONTHS_LIST } from "@/constants/calendar";
|
||||
import { MONTHS_LIST } from "@plane/constants";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { TGroupedIssues, TIssue, TIssueMap, TPaginationData, ICalendarDate
|
||||
import { cn, renderFormattedPayloadDate } from "@plane/utils";
|
||||
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
|
||||
// helpers
|
||||
import { MONTHS_LIST } from "@/constants/calendar";
|
||||
import { MONTHS_LIST } from "@plane/constants";
|
||||
// helpers
|
||||
// types
|
||||
import type { ICycleIssuesFilter } from "@/store/issue/cycle";
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { ChevronLeftIcon, ChevronRightIcon } from "@plane/propel/icons";
|
||||
// icons
|
||||
// constants
|
||||
import { getDate } from "@plane/utils";
|
||||
import { MONTHS_LIST } from "@/constants/calendar";
|
||||
import { MONTHS_LIST } from "@plane/constants";
|
||||
import { useCalendarView } from "@/hooks/store/use-calendar-view";
|
||||
import type { ICycleIssuesFilter } from "@/store/issue/cycle";
|
||||
import type { IModuleIssuesFilter } from "@/store/issue/module";
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import type { TCalendarLayouts, TSupportedFilterForUpdate } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
// types
|
||||
// constants
|
||||
import { CALENDAR_LAYOUTS } from "@/constants/calendar";
|
||||
import { CALENDAR_LAYOUTS } from "@plane/constants";
|
||||
import { useCalendarView } from "@/hooks/store/use-calendar-view";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
import type { ICycleIssuesFilter } from "@/store/issue/cycle";
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { EStartOfTheWeek } from "@plane/types";
|
||||
import { getOrderedDays } from "@plane/utils";
|
||||
import { DAYS_LIST } from "@/constants/calendar";
|
||||
import { DAYS_LIST } from "@plane/constants";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
|
||||
// plane web hooks
|
||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||
import { useBulkOperationStatus } from "@/hooks/use-bulk-operation-status";
|
||||
|
||||
import { IssueLayoutHOC } from "../issue-layout-HOC";
|
||||
import { GanttQuickAddIssueButton, QuickAddIssueRoot } from "../quick-add";
|
||||
|
||||
@@ -29,7 +29,7 @@ import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
// plane web components
|
||||
import { IssueBulkOperationsRoot } from "@/plane-web/components/issues/bulk-operations";
|
||||
// plane web hooks
|
||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||
import { useBulkOperationStatus } from "@/hooks/use-bulk-operation-status";
|
||||
// utils
|
||||
import type { GroupDropLocation } from "../utils";
|
||||
import { getGroupByColumns, isWorkspaceLevel, isSubGrouped } from "../utils";
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useProject } from "@/hooks/store/use-project";
|
||||
// plane web components
|
||||
import { IssueBulkOperationsRoot } from "@/plane-web/components/issues/bulk-operations";
|
||||
// plane web hooks
|
||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||
import { useBulkOperationStatus } from "@/hooks/use-bulk-operation-status";
|
||||
// local imports
|
||||
import type { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { QuickAddIssueRoot, SpreadsheetAddIssueButton } from "../quick-add";
|
||||
|
||||
@@ -9,9 +9,7 @@ import { createContext } from "react";
|
||||
import type { UseFormReset, UseFormWatch } from "react-hook-form";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import type { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
// plane web imports
|
||||
import type { TIssuePropertyValues, TIssuePropertyValueErrors } from "@/plane-web/types/issue-types";
|
||||
import type { ISearchIssueResponse, TIssue, TIssuePropertyValues, TIssuePropertyValueErrors } from "@plane/types";
|
||||
import type { TIssueFields } from "@/plane-web/components/issues/issue-modal";
|
||||
|
||||
export type TPropertyValuesValidationProps = {
|
||||
|
||||
@@ -48,7 +48,7 @@ import { DeDupeButtonRoot } from "@/plane-web/components/de-dupe/de-dupe-button"
|
||||
import { DuplicateModalRoot } from "@/plane-web/components/de-dupe/duplicate-modal";
|
||||
import { IssueTypeSelect, WorkItemTemplateSelect } from "@/plane-web/components/issues/issue-modal";
|
||||
import { WorkItemModalAdditionalProperties } from "@/plane-web/components/issues/issue-modal/modal-additional-properties";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
import { useDebouncedDuplicateIssues } from "@/hooks/use-debounced-duplicate-issues";
|
||||
|
||||
export interface IssueFormProps {
|
||||
data?: Partial<TIssue>;
|
||||
|
||||
@@ -25,7 +25,7 @@ import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe/duplicate-popover";
|
||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues/issue-details/issue-type-switcher";
|
||||
// plane web hooks
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
import { useDebouncedDuplicateIssues } from "@/hooks/use-debounced-duplicate-issues";
|
||||
// services
|
||||
import { WorkItemVersionService } from "@/services/issue";
|
||||
// local components
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
import { useWorkItemProperties } from "@/plane-web/hooks/use-issue-properties";
|
||||
import { useWorkItemProperties } from "@/hooks/use-issue-properties";
|
||||
// local imports
|
||||
import type { TIssueOperations } from "../issue-detail";
|
||||
import { IssueView } from "./view";
|
||||
|
||||
@@ -21,7 +21,7 @@ import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-red
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web imports
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
|
||||
import type { TIssueRelationTypes } from "@/plane-web/types";
|
||||
import type { TIssueRelationTypes } from "@plane/types";
|
||||
// local imports
|
||||
import { useRelationOperations } from "../issue-detail-widgets/relations/helper";
|
||||
import { RelationIssueProperty } from "./properties";
|
||||
|
||||
@@ -10,7 +10,7 @@ import { observer } from "mobx-react";
|
||||
import type { TIssue, TIssueServiceType } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// Plane-web imports
|
||||
import type { TIssueRelationTypes } from "@/plane-web/types";
|
||||
import type { TIssueRelationTypes } from "@plane/types";
|
||||
// local imports
|
||||
import { RelationIssueListItem } from "./issue-list-item";
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
// helpers
|
||||
import { getSidebarNavigationItemIcon } from "@/plane-web/components/workspace/sidebar/helper";
|
||||
// types
|
||||
import type { TPersonalNavigationItemKey } from "@/types/navigation-preferences";
|
||||
import type { TPersonalNavigationItemKey } from "@plane/types";
|
||||
|
||||
type TCustomizeNavigationDialogProps = {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { TPartialProject } from "@/plane-web/types";
|
||||
import type { TPartialProject } from "@plane/types";
|
||||
// plane propel imports
|
||||
import { Logo } from "@plane/propel/emoji-icon-picker";
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
|
||||
@@ -23,8 +23,8 @@ import { usePageOperations } from "@/hooks/use-page-operations";
|
||||
// plane web components
|
||||
import { MovePageModal } from "@/plane-web/components/pages";
|
||||
// plane web hooks
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { usePageFlag } from "@/plane-web/hooks/use-page-flag";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
import { usePageFlag } from "@/hooks/use-page-flag";
|
||||
// store types
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
import type { TCustomEventHandlers } from "@/hooks/use-realtime-page-events";
|
||||
import { useRealtimePageEvents } from "@/hooks/use-realtime-page-events";
|
||||
import { EditorAIMenu } from "@/plane-web/components/pages";
|
||||
import type { TExtendedEditorExtensionsConfig } from "@/plane-web/hooks/pages";
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import type { TExtendedEditorExtensionsConfig } from "@/hooks/pages";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
import { useEditorFlagging } from "@/hooks/use-editor-flagging";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
// local imports
|
||||
|
||||
@@ -14,8 +14,8 @@ import { usePageFallback } from "@/hooks/use-page-fallback";
|
||||
// plane web import
|
||||
import type { PageUpdateHandler, TCustomEventHandlers } from "@/hooks/use-realtime-page-events";
|
||||
import { PageModals } from "@/plane-web/components/pages";
|
||||
import { usePagesPaneExtensions, useExtendedEditorProps } from "@/plane-web/hooks/pages";
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { usePagesPaneExtensions, useExtendedEditorProps } from "@/hooks/pages";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
// local imports
|
||||
|
||||
@@ -16,7 +16,7 @@ import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
import { useQueryParams } from "@/hooks/use-query-params";
|
||||
// plane web imports
|
||||
import type { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane";
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
// local imports
|
||||
|
||||
@@ -12,8 +12,8 @@ import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import type { ToolbarMenuItem } from "@/constants/editor";
|
||||
import { TOOLBAR_ITEMS, TYPOGRAPHY_ITEMS } from "@/constants/editor";
|
||||
import type { ToolbarMenuItem } from "@plane/editor";
|
||||
import { TOOLBAR_ITEMS, TYPOGRAPHY_ITEMS } from "@plane/editor";
|
||||
// local imports
|
||||
import { ColorDropdown } from "./color-dropdown";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PageLockControl } from "@/plane-web/components/pages/header/lock-contro
|
||||
import { PageMoveControl } from "@/plane-web/components/pages/header/move-control";
|
||||
import { PageShareControl } from "@/plane-web/components/pages/header/share-control";
|
||||
// plane web hooks
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
// local imports
|
||||
|
||||
@@ -17,8 +17,8 @@ import { FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
// plane web hooks
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { usePageStore } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
import { usePageStore } from "@/hooks/store";
|
||||
// local imports
|
||||
import { PageAppliedFiltersList } from "../list/applied-filters";
|
||||
import { PageFiltersSelection } from "../list/filters";
|
||||
|
||||
@@ -15,7 +15,7 @@ import { renderFormattedDate, getFileURL } from "@plane/utils";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { usePageOperations } from "@/hooks/use-page-operations";
|
||||
// plane web hooks
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
// local imports
|
||||
|
||||
@@ -16,8 +16,8 @@ import { BlockItemAction } from "@/components/pages/list/block-item-action";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web hooks
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { usePage } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
import { usePage } from "@/hooks/store";
|
||||
|
||||
type TPageListBlock = {
|
||||
pageId: string;
|
||||
|
||||
@@ -10,8 +10,8 @@ import type { TPageNavigationTabs } from "@plane/types";
|
||||
// components
|
||||
import { ListLayout } from "@/components/core/list";
|
||||
// plane web hooks
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { usePageStore } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
import { usePageStore } from "@/hooks/store";
|
||||
// local imports
|
||||
import { PageListBlock } from "./block";
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web hooks
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { usePageStore } from "@/plane-web/hooks/store";
|
||||
import type { EPageStoreType } from "@/hooks/store";
|
||||
import { usePageStore } from "@/hooks/store";
|
||||
// local imports
|
||||
import { PageForm } from "./page-form";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user