Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18865b46fe | |||
| 2d60337eac | |||
| f3ac26e5c9 | |||
| d5a55de17a | |||
| 6f497b024b | |||
| a3e8ee6045 | |||
| c1ac6e4244 | |||
| 6d98619082 | |||
| 52d3169542 | |||
| 5989b1a134 | |||
| 291bb5c899 | |||
| 2ef00efaab | |||
| c5f96466e9 | |||
| 35938b57af | |||
| 1b1b160c04 | |||
| 4149e84e62 | |||
| 9408e92e44 | |||
| e9680cab74 | |||
| 229610513a | |||
| f9d9c92c83 | |||
| 89588d4451 | |||
| 3eb911837c | |||
| 4b50b27a74 | |||
| f44db89f41 | |||
| 8c3189e1be | |||
| eee2145734 | |||
| 106710f3d0 |
@@ -83,7 +83,7 @@ jobs:
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
with:
|
||||
context: ./aio
|
||||
file: ./aio/Dockerfile-base-full
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
with:
|
||||
context: ./aio
|
||||
file: ./aio/Dockerfile-base-slim
|
||||
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
|
||||
@@ -367,7 +367,7 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
uses: softprops/action-gh-release@v2.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
with:
|
||||
|
||||
@@ -29,11 +29,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -59,6 +59,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for all branches and tags
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -146,7 +146,12 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Check if the file type is allowed
|
||||
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
|
||||
allowed_types = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/jpg",
|
||||
]
|
||||
if type not in allowed_types:
|
||||
return Response(
|
||||
{
|
||||
@@ -317,7 +322,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
|
||||
# Project Cover
|
||||
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
|
||||
project = Project.objects.filter(id=asset.workspace_id).first()
|
||||
project = Project.objects.filter(id=asset.project_id).first()
|
||||
if project is None:
|
||||
return
|
||||
# Delete the previous cover image
|
||||
@@ -387,7 +392,13 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Check if the file type is allowed
|
||||
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
|
||||
allowed_types = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/jpg",
|
||||
"image/gif",
|
||||
]
|
||||
if type not in allowed_types:
|
||||
return Response(
|
||||
{
|
||||
@@ -620,7 +631,13 @@ class ProjectAssetEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Check if the file type is allowed
|
||||
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
|
||||
allowed_types = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/jpg",
|
||||
"image/gif",
|
||||
]
|
||||
if type not in allowed_types:
|
||||
return Response(
|
||||
{
|
||||
@@ -738,6 +755,11 @@ class ProjectAssetEndpoint(BaseAPIView):
|
||||
|
||||
class ProjectBulkAssetEndpoint(BaseAPIView):
|
||||
|
||||
def save_project_cover(self, asset, project_id):
|
||||
project = Project.objects.get(id=project_id)
|
||||
project.cover_image_asset_id = asset.id
|
||||
project.save()
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def post(self, request, slug, project_id, entity_id):
|
||||
asset_ids = request.data.get("asset_ids", [])
|
||||
@@ -773,6 +795,7 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
|
||||
assets.update(
|
||||
project_id=project_id,
|
||||
)
|
||||
[self.save_project_cover(asset, project_id) for asset in assets]
|
||||
|
||||
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
|
||||
assets.update(
|
||||
|
||||
@@ -32,7 +32,6 @@ from plane.db.models import (
|
||||
Session,
|
||||
)
|
||||
from plane.license.models import Instance, InstanceAdmin
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from plane.utils.paginator import BasePaginator
|
||||
from plane.authentication.utils.host import user_ip
|
||||
from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
|
||||
@@ -49,7 +48,6 @@ class UserEndpoint(BaseViewSet):
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
@cache_response(60 * 60)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def retrieve(self, request):
|
||||
@@ -59,14 +57,12 @@ class UserEndpoint(BaseViewSet):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@cache_response(60 * 60)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def retrieve_user_settings(self, request):
|
||||
serialized_data = UserMeSettingsSerializer(request.user).data
|
||||
return Response(serialized_data, status=status.HTTP_200_OK)
|
||||
|
||||
@cache_response(60 * 60)
|
||||
def retrieve_instance_admin(self, request):
|
||||
instance = Instance.objects.first()
|
||||
is_admin = InstanceAdmin.objects.filter(
|
||||
@@ -76,19 +72,9 @@ class UserEndpoint(BaseViewSet):
|
||||
{"is_instance_admin": is_admin}, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/",
|
||||
)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/settings/",
|
||||
)
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(path="/api/users/me/")
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", multiple=True, user=False
|
||||
)
|
||||
def deactivate(self, request):
|
||||
# Check all workspace user is active
|
||||
user = self.get_object()
|
||||
@@ -235,7 +221,6 @@ class UserSessionEndpoint(BaseAPIView):
|
||||
|
||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
|
||||
@invalidate_cache(path="/api/users/me/")
|
||||
def patch(self, request):
|
||||
profile = Profile.objects.get(user_id=request.user.id)
|
||||
profile.is_onboarded = request.data.get("is_onboarded", False)
|
||||
@@ -247,7 +232,6 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
|
||||
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||
|
||||
@invalidate_cache(path="/api/users/me/")
|
||||
def patch(self, request):
|
||||
profile = Profile.objects.get(user_id=request.user.id)
|
||||
profile.is_tour_completed = request.data.get(
|
||||
@@ -305,7 +289,6 @@ class ProfileEndpoint(BaseAPIView):
|
||||
serializer = ProfileSerializer(profile)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@invalidate_cache("/api/users/me/settings/")
|
||||
def patch(self, request):
|
||||
profile = Profile.objects.get(user=request.user)
|
||||
serializer = ProfileSerializer(
|
||||
|
||||
@@ -44,7 +44,6 @@ from plane.db.models import (
|
||||
WorkspaceTheme,
|
||||
)
|
||||
from plane.app.permissions import ROLE, allow_permission
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
@@ -99,9 +98,6 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
.select_related("owner")
|
||||
)
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||
@invalidate_cache(path="/api/instances/", user=False)
|
||||
def create(self, request):
|
||||
try:
|
||||
serializer = WorkSpaceSerializer(data=request.data)
|
||||
@@ -147,7 +143,6 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
@cache_response(60 * 60 * 2)
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
@@ -159,8 +154,6 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
@@ -170,13 +163,6 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", multiple=True, user=False
|
||||
)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/settings/", multiple=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
@@ -190,7 +176,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
"owner",
|
||||
]
|
||||
|
||||
@cache_response(60 * 60 * 2)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def get(self, request):
|
||||
|
||||
@@ -40,7 +40,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
DraftIssue,
|
||||
)
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from plane.utils.cache import invalidate_cache
|
||||
|
||||
from .. import BaseViewSet
|
||||
|
||||
@@ -66,7 +66,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
.select_related("member")
|
||||
)
|
||||
|
||||
@cache_response(60 * 60 * 2)
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@@ -93,12 +92,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/members/",
|
||||
url_params=True,
|
||||
user=False,
|
||||
multiple=True,
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def partial_update(self, request, slug, pk):
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
@@ -127,16 +120,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/members/",
|
||||
url_params=True,
|
||||
user=False,
|
||||
multiple=True,
|
||||
)
|
||||
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", user=False, multiple=True
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def destroy(self, request, slug, pk):
|
||||
# Check the user role who is deleting the user
|
||||
|
||||
@@ -42,7 +42,7 @@ app.conf.beat_schedule = {
|
||||
},
|
||||
"run-every-6-hours-for-instance-trace": {
|
||||
"task": "plane.license.bgtasks.tracer.instance_traces",
|
||||
"schedule": crontab(hour="*/6"),
|
||||
"schedule": crontab(hour="*/6", minute=0),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ class InstanceSerializer(BaseSerializer):
|
||||
model = Instance
|
||||
exclude = [
|
||||
"license_key",
|
||||
"user_count"
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
|
||||
@@ -3,7 +3,7 @@ from celery import shared_task
|
||||
from opentelemetry import trace
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import Instance
|
||||
from plane.license.models import Instance, InstanceAdmin
|
||||
from plane.db.models import (
|
||||
User,
|
||||
Workspace,
|
||||
@@ -25,6 +25,7 @@ def instance_traces():
|
||||
|
||||
# Check if the instance is registered
|
||||
instance = Instance.objects.first()
|
||||
instance_admin = InstanceAdmin.objects.first()
|
||||
|
||||
# If instance is None then return
|
||||
if instance is None:
|
||||
@@ -52,6 +53,17 @@ def instance_traces():
|
||||
span.set_attribute(
|
||||
"is_telemetry_enabled", instance.is_telemetry_enabled
|
||||
)
|
||||
span.set_attribute(
|
||||
"is_support_required", instance.is_support_required
|
||||
)
|
||||
span.set_attribute("is_setup_done", instance.is_setup_done)
|
||||
span.set_attribute(
|
||||
"is_signup_screen_visited", instance.is_signup_screen_visited
|
||||
)
|
||||
span.set_attribute("is_verified", instance.is_verified)
|
||||
span.set_attribute("edition", instance.edition)
|
||||
span.set_attribute("domain", instance.domain)
|
||||
span.set_attribute("is_test", instance.is_test)
|
||||
span.set_attribute("user_count", user_count)
|
||||
span.set_attribute("workspace_count", workspace_count)
|
||||
span.set_attribute("project_count", project_count)
|
||||
@@ -62,6 +74,13 @@ def instance_traces():
|
||||
span.set_attribute("module_issue_count", module_issue_count)
|
||||
span.set_attribute("page_count", page_count)
|
||||
|
||||
if instance_admin:
|
||||
span.set_attribute("admin_email", instance_admin.user.email)
|
||||
span.set_attribute(
|
||||
"admin_name",
|
||||
f"{instance_admin.user.first_name} {instance_admin.user.last_name}",
|
||||
)
|
||||
|
||||
# Workspace details
|
||||
for workspace in Workspace.objects.all():
|
||||
# Count of all models
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Python imports
|
||||
import json
|
||||
import secrets
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
@@ -8,10 +9,7 @@ from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import Instance
|
||||
from plane.db.models import (
|
||||
User,
|
||||
)
|
||||
from plane.license.models import Instance, EditiontTypes
|
||||
from plane.license.bgtasks.tracer import instance_traces
|
||||
|
||||
|
||||
@@ -32,7 +30,6 @@ class Command(BaseCommand):
|
||||
payload = {
|
||||
"instance_key": settings.INSTANCE_KEY,
|
||||
"version": data.get("version", 0.1),
|
||||
"user_count": User.objects.filter(is_bot=False).count(),
|
||||
}
|
||||
return payload
|
||||
|
||||
@@ -54,11 +51,11 @@ class Command(BaseCommand):
|
||||
instance = Instance.objects.create(
|
||||
instance_name="Plane Community Edition",
|
||||
instance_id=secrets.token_hex(12),
|
||||
license_key=None,
|
||||
current_version=payload.get("version"),
|
||||
latest_version=payload.get("version"),
|
||||
last_checked_at=timezone.now(),
|
||||
user_count=payload.get("user_count", 0),
|
||||
is_test=os.environ.get("IS_TEST", "0") == "1",
|
||||
edition=EditiontTypes.PLANE_CE.value,
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Instance registered"))
|
||||
@@ -69,9 +66,10 @@ class Command(BaseCommand):
|
||||
payload = self.read_package_json()
|
||||
# Update the instance details
|
||||
instance.last_checked_at = timezone.now()
|
||||
instance.user_count = payload.get("user_count", 0)
|
||||
instance.current_version = payload.get("version")
|
||||
instance.latest_version = payload.get("version")
|
||||
instance.is_test = os.environ.get("IS_TEST", "0") == "1"
|
||||
instance.edition = EditiontTypes.PLANE_CE.value
|
||||
instance.save()
|
||||
|
||||
# Call the instance traces task
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.2.15 on 2024-11-19 14:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('license', '0004_changelog_deleted_at_instance_deleted_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='instance',
|
||||
old_name='product',
|
||||
new_name='edition',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='instance',
|
||||
name='license_key',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='instance',
|
||||
name='user_count',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='instance',
|
||||
name='is_test',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1 +1,6 @@
|
||||
from .instance import Instance, InstanceAdmin, InstanceConfiguration
|
||||
from .instance import (
|
||||
Instance,
|
||||
InstanceAdmin,
|
||||
InstanceConfiguration,
|
||||
EditiontTypes,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from plane.db.models import BaseModel
|
||||
ROLE_CHOICES = ((20, "Admin"),)
|
||||
|
||||
|
||||
class ProductTypes(Enum):
|
||||
class EditiontTypes(Enum):
|
||||
PLANE_CE = "plane-ce"
|
||||
|
||||
|
||||
@@ -20,11 +20,10 @@ class Instance(BaseModel):
|
||||
instance_name = models.CharField(max_length=255)
|
||||
whitelist_emails = models.TextField(blank=True, null=True)
|
||||
instance_id = models.CharField(max_length=255, unique=True)
|
||||
license_key = models.CharField(max_length=256, null=True, blank=True)
|
||||
current_version = models.CharField(max_length=255)
|
||||
latest_version = models.CharField(max_length=255, null=True, blank=True)
|
||||
product = models.CharField(
|
||||
max_length=255, default=ProductTypes.PLANE_CE.value
|
||||
edition = models.CharField(
|
||||
max_length=255, default=EditiontTypes.PLANE_CE.value
|
||||
)
|
||||
domain = models.TextField(blank=True)
|
||||
# Instance specifics
|
||||
@@ -37,9 +36,8 @@ class Instance(BaseModel):
|
||||
is_setup_done = models.BooleanField(default=False)
|
||||
# signup screen
|
||||
is_signup_screen_visited = models.BooleanField(default=False)
|
||||
# users
|
||||
user_count = models.PositiveBigIntegerField(default=0)
|
||||
is_verified = models.BooleanField(default=False)
|
||||
is_test = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Instance"
|
||||
|
||||
@@ -35,8 +35,6 @@ SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = int(os.environ.get("DEBUG", "0"))
|
||||
|
||||
# Initialize Django instrumentation
|
||||
DjangoInstrumentor().instrument()
|
||||
# Configure the tracer provider
|
||||
service_name = os.environ.get("SERVICE_NAME", "plane-ce-api")
|
||||
resource = Resource.create({"service.name": service_name})
|
||||
@@ -46,6 +44,8 @@ otel_endpoint = os.environ.get("OTLP_ENDPOINT", "https://telemetry.plane.so")
|
||||
otlp_exporter = OTLPSpanExporter(endpoint=otel_endpoint)
|
||||
span_processor = BatchSpanProcessor(otlp_exporter)
|
||||
trace.get_tracer_provider().add_span_processor(span_processor)
|
||||
# Initialize Django instrumentation
|
||||
DjangoInstrumentor().instrument()
|
||||
|
||||
|
||||
# Allowed Hosts
|
||||
|
||||
@@ -63,7 +63,7 @@ pytz==2024.1
|
||||
# jwt
|
||||
PyJWT==2.8.0
|
||||
# OpenTelemetry
|
||||
opentelemetry-api==1.27.0
|
||||
opentelemetry-sdk==1.27.0
|
||||
opentelemetry-instrumentation-django==0.48b0
|
||||
opentelemetry-exporter-otlp==1.27.0
|
||||
opentelemetry-api==1.28.1
|
||||
opentelemetry-sdk==1.28.1
|
||||
opentelemetry-instrumentation-django==0.49b1
|
||||
opentelemetry-exporter-otlp==1.28.1
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"turbo": "^2.1.1"
|
||||
"turbo": "^2.3.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"name": "plane"
|
||||
|
||||
@@ -27,10 +27,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper table th {
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
background-color: rgba(var(--color-background-90));
|
||||
.table-wrapper table {
|
||||
th {
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr[background="none"],
|
||||
tr:not([background]) {
|
||||
th {
|
||||
background-color: rgba(var(--color-background-90));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper table .selectedCell {
|
||||
|
||||
+5
-1
@@ -18,7 +18,11 @@ export interface IProject {
|
||||
close_in: number;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
cover_image_url: string;
|
||||
// only for uploading the cover image
|
||||
cover_image_asset?: null;
|
||||
cover_image?: string;
|
||||
// only for rendering the cover image
|
||||
cover_image_url: readonly string;
|
||||
cycle_view: boolean;
|
||||
issue_views_view: boolean;
|
||||
module_view: boolean;
|
||||
|
||||
Vendored
+13
-4
@@ -3,7 +3,6 @@ import { TUserPermissions } from "./enums";
|
||||
|
||||
type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google";
|
||||
|
||||
|
||||
export interface IUserLite {
|
||||
avatar_url: string;
|
||||
display_name: string;
|
||||
@@ -14,7 +13,11 @@ export interface IUserLite {
|
||||
last_name: string;
|
||||
}
|
||||
export interface IUser extends IUserLite {
|
||||
cover_image_url: string | null;
|
||||
// only for uploading the cover image
|
||||
cover_image_asset?: string | null;
|
||||
cover_image?: string | null;
|
||||
// only for rendering the cover image
|
||||
cover_image_url: readonly (string | null);
|
||||
date_joined: string;
|
||||
email: string;
|
||||
is_active: boolean;
|
||||
@@ -90,7 +93,6 @@ export interface IUserTheme {
|
||||
sidebarBackground: string | undefined;
|
||||
}
|
||||
|
||||
|
||||
export interface IUserMemberLite extends IUserLite {
|
||||
email?: string;
|
||||
}
|
||||
@@ -153,7 +155,14 @@ export interface IUserProfileProjectSegregation {
|
||||
id: string;
|
||||
pending_issues: number;
|
||||
}[];
|
||||
user_data: Pick<IUser, "avatar_url" | "cover_image_url" | "display_name" | "first_name" | "last_name"> & {
|
||||
user_data: Pick<
|
||||
IUser,
|
||||
| "avatar_url"
|
||||
| "cover_image_url"
|
||||
| "display_name"
|
||||
| "first_name"
|
||||
| "last_name"
|
||||
> & {
|
||||
date_joined: Date;
|
||||
user_timezone: string;
|
||||
};
|
||||
|
||||
Vendored
+2
-2
@@ -40,8 +40,8 @@ export type TIssueOrderByOptions =
|
||||
| "-issue_cycle__cycle__name"
|
||||
| "target_date"
|
||||
| "-target_date"
|
||||
| "estimate_point"
|
||||
| "-estimate_point"
|
||||
| "estimate_point__key"
|
||||
| "-estimate_point__key"
|
||||
| "start_date"
|
||||
| "-start_date"
|
||||
| "link_count"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
@@ -62,6 +62,11 @@ const WorkspaceDashboardPage = observer(() => {
|
||||
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
|
||||
);
|
||||
|
||||
const embedRemoveCurrentNotification = useCallback(
|
||||
() => setCurrentSelectedNotificationId(undefined),
|
||||
[setCurrentSelectedNotificationId]
|
||||
);
|
||||
|
||||
// clearing up the selected notifications when unmounting the page
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -95,15 +100,12 @@ const WorkspaceDashboardPage = observer(() => {
|
||||
projectId={project_id}
|
||||
inboxIssueId={issue_id}
|
||||
isNotificationEmbed
|
||||
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
|
||||
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<IssuePeekOverview
|
||||
embedIssue
|
||||
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
|
||||
/>
|
||||
<IssuePeekOverview embedIssue embedRemoveCurrentNotification={embedRemoveCurrentNotification} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -70,11 +70,15 @@ const ProfileSettingsPage = observer(() => {
|
||||
first_name: formData.first_name,
|
||||
last_name: formData.last_name,
|
||||
avatar_url: formData.avatar_url,
|
||||
cover_image_url: formData.cover_image_url,
|
||||
role: formData.role,
|
||||
display_name: formData?.display_name,
|
||||
user_timezone: formData.user_timezone,
|
||||
};
|
||||
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
|
||||
if (formData.cover_image_url?.startsWith("http")) {
|
||||
payload.cover_image = formData.cover_image_url;
|
||||
payload.cover_image_asset = null;
|
||||
}
|
||||
|
||||
const updateCurrentUserDetail = updateCurrentUser(payload).finally(() => setIsLoading(false));
|
||||
setPromiseToast(updateCurrentUserDetail, {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./maintenance-message";
|
||||
@@ -0,0 +1,6 @@
|
||||
export const MaintenanceMessage = () => (
|
||||
<h1 className="text-xl font-medium text-custom-text-100 text-center md:text-left">
|
||||
Plane didn't start up. This could be because one or more Plane services failed to start. <br /> Choose View
|
||||
Logs from setup.sh and Docker logs to be sure.
|
||||
</h1>
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC, Fragment } from "react";
|
||||
|
||||
export const MaintenanceMode: FC = () => <Fragment />;
|
||||
@@ -74,6 +74,11 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
|
||||
// Upper case identifier
|
||||
formData.identifier = formData.identifier?.toUpperCase();
|
||||
const coverImage = formData.cover_image_url;
|
||||
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
|
||||
if (coverImage?.startsWith("http")) {
|
||||
formData.cover_image = coverImage;
|
||||
formData.cover_image_asset = null;
|
||||
}
|
||||
|
||||
return createProject(workspaceSlug.toString(), formData)
|
||||
.then(async (res) => {
|
||||
|
||||
@@ -5,7 +5,11 @@ import { computedFn } from "mobx-utils";
|
||||
// components
|
||||
import { ChartDataType, IBlockUpdateDependencyData, IGanttBlock, TGanttViews } from "@/components/gantt-chart";
|
||||
import { currentViewDataWithView } from "@/components/gantt-chart/data";
|
||||
import { getDateFromPositionOnGantt, getItemPositionWidth } from "@/components/gantt-chart/views/helpers";
|
||||
import {
|
||||
getDateFromPositionOnGantt,
|
||||
getItemPositionWidth,
|
||||
getPositionFromDate,
|
||||
} from "@/components/gantt-chart/views/helpers";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// store
|
||||
@@ -47,6 +51,7 @@ export interface IBaseTimelineStore {
|
||||
initGantt: () => void;
|
||||
|
||||
getDateFromPositionOnGantt: (position: number, offsetDays: number) => Date | undefined;
|
||||
getPositionFromDateOnGantt: (date: string | Date, offSetWidth: number) => number | undefined;
|
||||
}
|
||||
|
||||
export class BaseTimeLineStore implements IBaseTimelineStore {
|
||||
@@ -186,7 +191,7 @@ export class BaseTimeLineStore implements IBaseTimelineStore {
|
||||
start_date: blockData?.start_date ?? undefined,
|
||||
target_date: blockData?.target_date ?? undefined,
|
||||
};
|
||||
if (this.currentViewData && this.currentViewData?.data?.startDate && this.currentViewData?.data?.dayWidth) {
|
||||
if (this.currentViewData && (this.currentViewData?.data?.startDate || this.currentViewData?.data?.dayWidth)) {
|
||||
block.position = getItemPositionWidth(this.currentViewData, block);
|
||||
}
|
||||
|
||||
@@ -227,6 +232,15 @@ export class BaseTimeLineStore implements IBaseTimelineStore {
|
||||
return Math.round(position / this.currentViewData.data.dayWidth);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns position of the date on chart
|
||||
*/
|
||||
getPositionFromDateOnGantt = computedFn((date: string | Date, offSetWidth: number) => {
|
||||
if (!this.currentViewData) return;
|
||||
|
||||
return getPositionFromDate(this.currentViewData, date, offSetWidth);
|
||||
});
|
||||
|
||||
/**
|
||||
* returns the date at which the position corresponds to on the timeline chart
|
||||
*/
|
||||
|
||||
@@ -68,7 +68,7 @@ export const BlockRow: React.FC<Props> = observer((props) => {
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || !block.data || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
|
||||
|
||||
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
||||
const isBlockVisibleOnChart = block.start_date || block.target_date;
|
||||
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
|
||||
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
|
||||
const isBlockHoveredOn = isBlockActive(block.id);
|
||||
|
||||
@@ -46,10 +46,11 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
|
||||
const { isMoving, handleBlockDrag } = useGanttResizable(block, resizableRef, ganttContainerRef, updateBlockDates);
|
||||
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
|
||||
const isBlockVisibleOnChart = block?.start_date || block?.target_date;
|
||||
const isBlockComplete = block?.start_date && block?.target_date;
|
||||
|
||||
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || (!showAllBlocks && !isBlockVisibleOnChart)) return null;
|
||||
|
||||
if (!block.data) return null;
|
||||
|
||||
@@ -63,7 +64,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
ref={resizableRef}
|
||||
style={{
|
||||
height: `${BLOCK_HEIGHT}px`,
|
||||
transform: `translateX(${block.position?.marginLeft}px)`,
|
||||
marginLeft: `${block.position?.marginLeft}px`,
|
||||
width: `${block.position?.width}px`,
|
||||
}}
|
||||
>
|
||||
@@ -88,7 +89,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
handleBlockDrag={handleBlockDrag}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableBlockMove={enableBlockMove && !!isBlockComplete}
|
||||
isMoving={isMoving}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,7 @@ import { IssueBulkOperationsRoot } from "@/plane-web/components/issues";
|
||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||
//
|
||||
import { GanttChartRowList } from "../blocks/block-row-list";
|
||||
import { GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
|
||||
import { DEFAULT_BLOCK_WIDTH, GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
|
||||
import { getItemPositionWidth } from "../views";
|
||||
import { TimelineDragHelper } from "./timeline-drag-helper";
|
||||
|
||||
@@ -108,14 +108,20 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
|
||||
const approxRangeLeft = scrollLeft;
|
||||
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
|
||||
const calculatedRangeRight = itemsContainerWidth - (scrollLeft + clientWidth);
|
||||
|
||||
if (approxRangeRight < clientWidth) updateCurrentViewRenderPayload("right", currentView);
|
||||
if (approxRangeLeft < clientWidth) updateCurrentViewRenderPayload("left", currentView);
|
||||
if (approxRangeRight < clientWidth || calculatedRangeRight < clientWidth) {
|
||||
updateCurrentViewRenderPayload("right", currentView);
|
||||
}
|
||||
if (approxRangeLeft < clientWidth) {
|
||||
updateCurrentViewRenderPayload("left", currentView);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollToBlock = (block: IGanttBlock) => {
|
||||
const scrollContainer = ganttContainerRef.current as HTMLDivElement;
|
||||
const scrollToDate = getDate(block.start_date);
|
||||
const scrollToEndDate = !block.start_date && block.target_date;
|
||||
const scrollToDate = block.start_date ? getDate(block.start_date) : getDate(block.target_date);
|
||||
let chartData;
|
||||
|
||||
if (!scrollContainer || !currentViewData || !scrollToDate) return;
|
||||
@@ -129,7 +135,8 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
const updatedPosition = getItemPositionWidth(chartData ?? currentViewData, block);
|
||||
|
||||
setTimeout(() => {
|
||||
if (updatedPosition) scrollContainer.scrollLeft = updatedPosition.marginLeft - 4;
|
||||
if (updatedPosition)
|
||||
scrollContainer.scrollLeft = updatedPosition.marginLeft - 4 - (scrollToEndDate ? DEFAULT_BLOCK_WIDTH : 0);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -189,6 +196,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
style={{
|
||||
width: `${itemsContainerWidth}px`,
|
||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||
paddingBottom: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
<GanttChartRowList
|
||||
|
||||
@@ -25,7 +25,7 @@ export const MonthChartView: FC<any> = observer(() => {
|
||||
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
|
||||
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
|
||||
{currentViewData && (
|
||||
<div className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200">
|
||||
{/** Header Div */}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const QuarterChartView: FC<any> = observer(() => {
|
||||
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
|
||||
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
|
||||
{currentViewData &&
|
||||
quarterBlocks?.map((quarterBlock, rootIndex) => (
|
||||
<div
|
||||
|
||||
@@ -13,7 +13,7 @@ export const WeekChartView: FC<any> = observer(() => {
|
||||
const weekBlocks: IWeekBlock[] = renderView;
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
|
||||
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
|
||||
{currentViewData &&
|
||||
weekBlocks?.map((block, rootIndex) => (
|
||||
<div
|
||||
|
||||
@@ -6,4 +6,6 @@ export const GANTT_BREADCRUMBS_HEIGHT = 40;
|
||||
|
||||
export const SIDEBAR_WIDTH = 360;
|
||||
|
||||
export const DEFAULT_BLOCK_WIDTH = 60;
|
||||
|
||||
export const GANTT_SELECT_GROUP = "gantt-issues";
|
||||
|
||||
@@ -71,7 +71,7 @@ export const useGanttResizable = (
|
||||
// calculate new marginLeft and update the initial marginLeft to the newly calculated one
|
||||
marginLeft = Math.round(mouseX / dayWidth) * dayWidth;
|
||||
// get Dimensions from dom's style
|
||||
const prevMarginLeft = parseFloat(resizableDiv.style.transform.slice(11, -3));
|
||||
const prevMarginLeft = parseFloat(resizableDiv.style.marginLeft.slice(0, -2));
|
||||
const prevWidth = parseFloat(resizableDiv.style.width.slice(0, -2));
|
||||
// calculate new width
|
||||
const marginDelta = prevMarginLeft - marginLeft;
|
||||
@@ -88,7 +88,7 @@ export const useGanttResizable = (
|
||||
if (width < dayWidth) return;
|
||||
|
||||
resizableDiv.style.width = `${width}px`;
|
||||
resizableDiv.style.transform = `translateX(${marginLeft}px)`;
|
||||
resizableDiv.style.marginLeft = `${marginLeft}px`;
|
||||
|
||||
const deltaLeft = Math.round((marginLeft - (block.position?.marginLeft ?? 0)) / dayWidth) * dayWidth;
|
||||
const deltaWidth = Math.round((width - (block.position?.width ?? 0)) / dayWidth) * dayWidth;
|
||||
|
||||
@@ -34,7 +34,7 @@ export const GanttDnDHOC = observer((props: Props) => {
|
||||
draggable({
|
||||
element,
|
||||
canDrag: () => isDragEnabled,
|
||||
getInitialData: () => ({ id }),
|
||||
getInitialData: () => ({ id, dragInstanceId: "GANTT_REORDER" }),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
@@ -44,7 +44,7 @@ export const GanttDnDHOC = observer((props: Props) => {
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop: ({ source }) => source?.data?.id !== id,
|
||||
canDrop: ({ source }) => source?.data?.id !== id && source?.data?.dragInstanceId === "GANTT_REORDER",
|
||||
getData: ({ input, element }) => {
|
||||
const data = { id };
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ export const IssuesSidebarBlock = observer((props: Props) => {
|
||||
const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
|
||||
const { getIsIssuePeeked } = useIssueDetail();
|
||||
|
||||
const isBlockVisibleOnChart = !!block?.start_date && !!block?.target_date;
|
||||
const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
const isBlockComplete = !!block?.start_date && !!block?.target_date;
|
||||
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
|
||||
if (!block?.data) return null;
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
|
||||
if (!block) return <></>;
|
||||
|
||||
const isBlockVisibleOnChart = !!block.start_date && !!block.target_date;
|
||||
const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
const isBlockComplete = !!block.start_date && !!block.target_date;
|
||||
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { addDaysToDate, findTotalDaysInRange, getDate } from "@/helpers/date-time.helper";
|
||||
import { DEFAULT_BLOCK_WIDTH } from "../constants";
|
||||
import { ChartDataType, IGanttBlock } from "../types";
|
||||
import { IMonthBlock, IMonthView, monthView } from "./month-view";
|
||||
import { quarterView } from "./quarter-view";
|
||||
import { IWeekBlock, weekView } from "./week-view";
|
||||
|
||||
/**
|
||||
* Generates Date by using Day, month and Year
|
||||
@@ -84,7 +82,7 @@ export const getDateFromPositionOnGantt = (position: number, chartData: ChartDat
|
||||
*/
|
||||
export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttBlock) => {
|
||||
let scrollPosition: number = 0;
|
||||
let scrollWidth: number = 0;
|
||||
let scrollWidth: number = DEFAULT_BLOCK_WIDTH;
|
||||
|
||||
const { startDate: chartStartDate } = chartData.data;
|
||||
const { start_date, target_date } = itemData;
|
||||
@@ -92,24 +90,42 @@ export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttB
|
||||
const itemStartDate = getDate(start_date);
|
||||
const itemTargetDate = getDate(target_date);
|
||||
|
||||
if (!itemStartDate || !itemTargetDate) return;
|
||||
|
||||
chartStartDate.setHours(0, 0, 0, 0);
|
||||
itemStartDate.setHours(0, 0, 0, 0);
|
||||
itemTargetDate.setHours(0, 0, 0, 0);
|
||||
itemStartDate?.setHours(0, 0, 0, 0);
|
||||
itemTargetDate?.setHours(0, 0, 0, 0);
|
||||
|
||||
// get number of days from chart start date to block's start date
|
||||
const positionDaysDifference = Math.round(findTotalDaysInRange(chartStartDate, itemStartDate, false) ?? 0);
|
||||
|
||||
if (!positionDaysDifference) return;
|
||||
if (!itemStartDate && !itemTargetDate) return;
|
||||
|
||||
// get scroll position from the number of days and width of each day
|
||||
scrollPosition = positionDaysDifference * chartData.data.dayWidth;
|
||||
scrollPosition = itemStartDate
|
||||
? getPositionFromDate(chartData, itemStartDate, 0)
|
||||
: getPositionFromDate(chartData, itemTargetDate!, -1 * DEFAULT_BLOCK_WIDTH);
|
||||
|
||||
// get width of block
|
||||
const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime();
|
||||
const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)));
|
||||
scrollWidth = (widthDaysDifference + 1) * chartData.data.dayWidth;
|
||||
if (itemStartDate && itemTargetDate) {
|
||||
// get width of block
|
||||
const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime();
|
||||
const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)));
|
||||
scrollWidth = (widthDaysDifference + 1) * chartData.data.dayWidth;
|
||||
}
|
||||
|
||||
return { marginLeft: scrollPosition, width: scrollWidth };
|
||||
};
|
||||
|
||||
export const getPositionFromDate = (chartData: ChartDataType, date: string | Date, offsetWidth: number) => {
|
||||
const currDate = getDate(date);
|
||||
|
||||
const { startDate: chartStartDate } = chartData.data;
|
||||
|
||||
if (!currDate || !chartStartDate) return 0;
|
||||
|
||||
chartStartDate.setHours(0, 0, 0, 0);
|
||||
currDate.setHours(0, 0, 0, 0);
|
||||
|
||||
// get number of days from chart start date to block's start date
|
||||
const positionDaysDifference = Math.round(findTotalDaysInRange(chartStartDate, currDate, false) ?? 0);
|
||||
|
||||
if (!positionDaysDifference) return 0;
|
||||
|
||||
// get scroll position from the number of days and width of each day
|
||||
return positionDaysDifference * chartData.data.dayWidth + offsetWidth;
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./not-ready-view";
|
||||
export * from "./maintenance-view";
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Image from "next/image";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// layouts
|
||||
import DefaultLayout from "@/layouts/default-layout";
|
||||
// components
|
||||
import { MaintenanceMessage } from "@/plane-web/components/instance";
|
||||
// images
|
||||
import maintenanceModeImage from "@/public/maintenance-mode.webp";
|
||||
|
||||
export const MaintenanceView: FC = () => (
|
||||
<DefaultLayout>
|
||||
<div className="relative container mx-auto h-full w-full flex flex-col md:flex-row gap-2 items-center justify-center gap-y-5 bg-custom-background-100 text-center">
|
||||
<div className="relative w-full">
|
||||
<Image
|
||||
src={maintenanceModeImage}
|
||||
height="176"
|
||||
width="288"
|
||||
alt="ProjectSettingImg"
|
||||
className="w-full h-full object-fill object-center"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full space-y-4 relative flex flex-col justify-center md:justify-start items-center md:items-start">
|
||||
<MaintenanceMessage />
|
||||
<Button variant="outline-primary" onClick={() => window.location.reload()}>
|
||||
Reload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
);
|
||||
@@ -27,6 +27,7 @@ export const useLinkOperations = (workspaceSlug: string, projectId: string, issu
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Link not created",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
update: async (linkId: string, data: Partial<TIssueLink>) => {
|
||||
@@ -44,6 +45,7 @@ export const useLinkOperations = (workspaceSlug: string, projectId: string, issu
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Link not updated",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
remove: async (linkId: string) => {
|
||||
|
||||
@@ -7,8 +7,6 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import type { TIssueLinkEditableFields } from "@plane/types";
|
||||
// plane ui
|
||||
import { Button, Input, ModalCore } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// types
|
||||
@@ -48,14 +46,18 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
|
||||
|
||||
const onClose = () => {
|
||||
setIssueLinkData(null);
|
||||
reset();
|
||||
if (handleOnClose) handleOnClose();
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: TIssueLinkCreateFormFieldOptions) => {
|
||||
if (!formData || !formData.id) await linkOperations.create({ title: formData.title, url: formData.url });
|
||||
else await linkOperations.update(formData.id as string, { title: formData.title, url: formData.url });
|
||||
onClose();
|
||||
const parsedUrl = formData.url.startsWith("http") ? formData.url : `http://${formData.url}`;
|
||||
try {
|
||||
if (!formData || !formData.id) await linkOperations.create({ title: formData.title, url: parsedUrl });
|
||||
else await linkOperations.update(formData.id, { title: formData.title, url: parsedUrl });
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,7 +79,6 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
|
||||
name="url"
|
||||
rules={{
|
||||
required: "URL is required",
|
||||
validate: (value) => checkURLValidity(value) || "URL is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
|
||||
@@ -58,6 +58,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Link not created",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
update: async (linkId: string, data: Partial<TIssueLink>) => {
|
||||
@@ -76,6 +77,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Link not updated",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
remove: async (linkId: string) => {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { Tooltip, ControlLink } from "@plane/ui";
|
||||
// components
|
||||
import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
@@ -13,7 +15,8 @@ import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-red
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// local types
|
||||
//
|
||||
import { getBlockViewDetails } from "../utils";
|
||||
import { GanttStoreType } from "./base-gantt-root";
|
||||
|
||||
type Props = {
|
||||
@@ -39,36 +42,37 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
|
||||
const stateDetails =
|
||||
issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id);
|
||||
|
||||
const { message, blockStyle } = getBlockViewDetails(issueDetails, stateDetails?.color ?? "");
|
||||
|
||||
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`issue-${issueId}`}
|
||||
className="relative flex h-full w-full cursor-pointer items-center rounded"
|
||||
style={{
|
||||
backgroundColor: stateDetails?.color,
|
||||
}}
|
||||
onClick={handleIssuePeekOverview}
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
<div className="space-y-1">
|
||||
<h5>{issueDetails?.name}</h5>
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
}
|
||||
position="top-left"
|
||||
disabled={!message}
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
<div className="space-y-1">
|
||||
<h5>{issueDetails?.name}</h5>
|
||||
<div>
|
||||
{renderFormattedDate(issueDetails?.start_date ?? "")} to{" "}
|
||||
{renderFormattedDate(issueDetails?.target_date ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
position="top-left"
|
||||
<div
|
||||
id={`issue-${issueId}`}
|
||||
className="relative flex h-full w-full cursor-pointer items-center rounded"
|
||||
style={blockStyle}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100">
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<div
|
||||
className="sticky w-auto overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100"
|
||||
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
||||
>
|
||||
{issueDetails?.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -92,7 +96,11 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
// derived values
|
||||
const issueDetails = getIssueById(issueId);
|
||||
|
||||
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||
const handleIssuePeekOverview = (e: any) => {
|
||||
e.stopPropagation(true);
|
||||
e.preventDefault();
|
||||
handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||
};
|
||||
|
||||
return (
|
||||
<ControlLink
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import clone from "lodash/clone";
|
||||
import concat from "lodash/concat";
|
||||
@@ -32,6 +33,7 @@ import { Logo } from "@/components/common";
|
||||
import { ISSUE_PRIORITIES, EIssuesStoreType } from "@/constants/issue";
|
||||
import { STATE_GROUPS } from "@/constants/state";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// store
|
||||
import { ICycleStore } from "@/store/cycle.store";
|
||||
@@ -672,3 +674,39 @@ export function getApproximateCardHeight(displayProperties: IIssueDisplayPropert
|
||||
|
||||
return cardHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* This Method is used to get Block view details, that returns block style and tooltip message
|
||||
* @param block
|
||||
* @param backgroundColor
|
||||
* @returns
|
||||
*/
|
||||
export const getBlockViewDetails = (
|
||||
block: { start_date: string | undefined | null; target_date: string | undefined | null } | undefined | null,
|
||||
backgroundColor: string
|
||||
) => {
|
||||
const isBlockVisibleOnChart = block?.start_date || block?.target_date;
|
||||
const isBlockComplete = block?.start_date && block?.target_date;
|
||||
|
||||
let message;
|
||||
const blockStyle: CSSProperties = {
|
||||
backgroundColor,
|
||||
};
|
||||
|
||||
if (isBlockVisibleOnChart && !isBlockComplete) {
|
||||
if (block?.start_date) {
|
||||
message = `From ${renderFormattedDate(block.start_date)}`;
|
||||
blockStyle.maskImage = `linear-gradient(to right, ${backgroundColor} 50%, transparent 95%)`;
|
||||
} else if (block?.target_date) {
|
||||
message = `Till ${renderFormattedDate(block.target_date)}`;
|
||||
blockStyle.maskImage = `linear-gradient(to left, ${backgroundColor} 50%, transparent 95%)`;
|
||||
}
|
||||
} else if (isBlockComplete) {
|
||||
message = `${renderFormattedDate(block?.start_date)} to ${renderFormattedDate(block?.target_date)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
blockStyle,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -135,8 +135,13 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
<span>Created by</span>
|
||||
</div>
|
||||
<div className="w-full h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-not-allowed">
|
||||
<ButtonAvatars showTooltip userIds={createdByDetails?.id} />
|
||||
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
|
||||
<ButtonAvatars
|
||||
showTooltip
|
||||
userIds={createdByDetails?.display_name.includes("-intake") ? null : createdByDetails?.id}
|
||||
/>
|
||||
<span className="flex-grow truncate text-xs leading-5">
|
||||
{createdByDetails?.display_name.includes("-intake") ? "Plane" : createdByDetails?.display_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// hooks
|
||||
// ui
|
||||
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { MODULE_STATUS } from "@/constants/module";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// components
|
||||
import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
|
||||
import { getBlockViewDetails } from "@/components/issues/issue-layouts/utils";
|
||||
// constants
|
||||
import { MODULE_STATUS } from "@/constants/module";
|
||||
// hooks
|
||||
import { useModule } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
@@ -30,31 +31,40 @@ export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const { message, blockStyle } = getBlockViewDetails(
|
||||
moduleDetails,
|
||||
MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color ?? ""
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-full w-full items-center rounded"
|
||||
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color }}
|
||||
onClick={() =>
|
||||
router.push(`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`)
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
<div className="space-y-1">
|
||||
<h5>{moduleDetails?.name}</h5>
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
}
|
||||
position="top-left"
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
<div className="space-y-1">
|
||||
<h5>{moduleDetails?.name}</h5>
|
||||
<div>
|
||||
{renderFormattedDate(moduleDetails?.start_date ?? "")} to{" "}
|
||||
{renderFormattedDate(moduleDetails?.target_date ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="relative flex h-full w-full cursor-pointer items-center rounded"
|
||||
style={blockStyle}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`
|
||||
)
|
||||
}
|
||||
position="top-left"
|
||||
>
|
||||
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{moduleDetails?.name}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<div
|
||||
className="sticky w-auto overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100"
|
||||
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
||||
>
|
||||
{moduleDetails?.name}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import type { ILinkDetails, ModuleLink } from "@plane/types";
|
||||
// plane ui
|
||||
import { Button, Input, ModalCore, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
|
||||
type Props = {
|
||||
createLink: (formData: ModuleLink) => Promise<void>;
|
||||
@@ -39,9 +37,10 @@ export const CreateUpdateModuleLinkModal: FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: ModuleLink) => {
|
||||
const parsedUrl = formData.url.startsWith("http") ? formData.url : `http://${formData.url}`;
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
url: formData.url,
|
||||
url: parsedUrl,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -92,7 +91,6 @@ export const CreateUpdateModuleLinkModal: FC<Props> = (props) => {
|
||||
name="url"
|
||||
rules={{
|
||||
required: "URL is required",
|
||||
validate: (value) => checkURLValidity(value) || "URL is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
|
||||
@@ -144,10 +144,15 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||
network: formData.network,
|
||||
identifier: formData.identifier,
|
||||
description: formData.description,
|
||||
cover_image_url: formData.cover_image_url,
|
||||
|
||||
logo_props: formData.logo_props,
|
||||
// timezone: formData.timezone,
|
||||
};
|
||||
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
|
||||
if (formData.cover_image_url?.startsWith("http")) {
|
||||
payload.cover_image = formData.cover_image_url;
|
||||
payload.cover_image_asset = null;
|
||||
}
|
||||
|
||||
if (project.identifier !== formData.identifier)
|
||||
await projectService
|
||||
|
||||
@@ -79,9 +79,9 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
},
|
||||
estimate: {
|
||||
title: "Estimate",
|
||||
ascendingOrderKey: "estimate_point",
|
||||
ascendingOrderKey: "estimate_point__key",
|
||||
ascendingOrderTitle: "Low",
|
||||
descendingOrderKey: "-estimate_point",
|
||||
descendingOrderKey: "-estimate_point__key",
|
||||
descendingOrderTitle: "High",
|
||||
icon: Triangle,
|
||||
Column: SpreadsheetEstimateColumn,
|
||||
|
||||
@@ -3,11 +3,9 @@ import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { InstanceNotReady } from "@/components/instance";
|
||||
import { InstanceNotReady, MaintenanceView } from "@/components/instance";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { MaintenanceMode } from "@/plane-web/components/maintenance-mode";
|
||||
|
||||
type TInstanceWrapper = {
|
||||
children: ReactNode;
|
||||
@@ -32,7 +30,7 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (instanceSWRError) return <MaintenanceMode />;
|
||||
if (instanceSWRError) return <MaintenanceView />;
|
||||
|
||||
// something went wrong while in the request
|
||||
if (error && error?.status === "error") return <>{children}</>;
|
||||
|
||||
@@ -300,6 +300,7 @@ export class Storage {
|
||||
const { cursor, group_by, sub_group_by } = queries;
|
||||
|
||||
const query = issueFilterQueryConstructor(this.workspaceSlug, projectId, queries);
|
||||
log("#### Query", query);
|
||||
const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, queries);
|
||||
const start = performance.now();
|
||||
let issuesRaw: any[] = [];
|
||||
|
||||
@@ -87,10 +87,10 @@ export const getStates = async (workspaceSlug: string) => {
|
||||
export const getEstimatePoints = async (workspaceSlug: string) => {
|
||||
const estimateService = new EstimateService();
|
||||
const estimates = await estimateService.fetchWorkspaceEstimates(workspaceSlug);
|
||||
const objects: IEstimatePoint[] = [];
|
||||
let objects: IEstimatePoint[] = [];
|
||||
(estimates || []).forEach((estimate: IEstimate) => {
|
||||
if (estimate?.points) {
|
||||
objects.concat(estimate.points);
|
||||
objects = objects.concat(estimate.points);
|
||||
}
|
||||
});
|
||||
return objects;
|
||||
@@ -104,6 +104,9 @@ export const getMembers = async (workspaceSlug: string) => {
|
||||
};
|
||||
|
||||
export const loadWorkSpaceData = async (workspaceSlug: string) => {
|
||||
if (!persistence.db || !persistence.db.exec) {
|
||||
return;
|
||||
}
|
||||
log("Loading workspace data");
|
||||
const promises = [];
|
||||
promises.push(getLabels(workspaceSlug));
|
||||
@@ -112,7 +115,7 @@ export const loadWorkSpaceData = async (workspaceSlug: string) => {
|
||||
promises.push(getStates(workspaceSlug));
|
||||
promises.push(getEstimatePoints(workspaceSlug));
|
||||
promises.push(getMembers(workspaceSlug));
|
||||
const [labels, modules, cycles, states, estimates, memebers] = await Promise.all(promises);
|
||||
const [labels, modules, cycles, states, estimates, members] = await Promise.all(promises);
|
||||
|
||||
const start = performance.now();
|
||||
await persistence.db.exec("BEGIN;");
|
||||
@@ -121,7 +124,7 @@ export const loadWorkSpaceData = async (workspaceSlug: string) => {
|
||||
await batchInserts(cycles, "cycles", cycleSchema);
|
||||
await batchInserts(states, "states", stateSchema);
|
||||
await batchInserts(estimates, "estimate_points", estimatePointSchema);
|
||||
await batchInserts(memebers, "members", memberSchema);
|
||||
await batchInserts(members, "members", memberSchema);
|
||||
await persistence.db.exec("COMMIT;");
|
||||
const end = performance.now();
|
||||
log("Time taken to load workspace data", end - start);
|
||||
|
||||
@@ -18,6 +18,8 @@ export const SPECIAL_ORDER_BY = {
|
||||
"-issue_cycle__cycle__name": "cycles",
|
||||
state__name: "states",
|
||||
"-state__name": "states",
|
||||
estimate_point__key: "estimate_point",
|
||||
"-estimate_point__key": "estimate_point",
|
||||
};
|
||||
export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: string, queries: any) => {
|
||||
const {
|
||||
@@ -48,8 +50,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
|
||||
`;
|
||||
|
||||
log("###", sql);
|
||||
|
||||
return sql;
|
||||
}
|
||||
if (group_by) {
|
||||
@@ -64,8 +64,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
WHERE rank <= ${per_page}
|
||||
`;
|
||||
|
||||
log("###", sql);
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
@@ -78,8 +76,10 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
sql += `SELECT fi.* , `;
|
||||
if (order_by.includes("assignee")) {
|
||||
sql += ` s.first_name as ${name} `;
|
||||
} else if (order_by.includes("estimate")) {
|
||||
sql += ` s.key as ${name} `;
|
||||
} else {
|
||||
sql += ` s.name as ${name} `;
|
||||
sql += ` s.name as ${name} `;
|
||||
}
|
||||
sql += `FROM fi `;
|
||||
if (order_by && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
|
||||
@@ -87,7 +87,7 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
sql += `
|
||||
LEFT JOIN cycles s on fi.cycle_id = s.id`;
|
||||
}
|
||||
if (order_by.includes("estimate_point")) {
|
||||
if (order_by.includes("estimate_point__key")) {
|
||||
sql += `
|
||||
LEFT JOIN estimate_points s on fi.estimate_point = s.id`;
|
||||
}
|
||||
@@ -120,7 +120,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
`;
|
||||
sql += ` group by i.id ${orderByString} LIMIT ${pageSize} OFFSET ${offset * 1 + page * pageSize};`;
|
||||
|
||||
log("######$$$", sql);
|
||||
return sql;
|
||||
}
|
||||
|
||||
@@ -149,7 +148,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
// Add offset and paging to query
|
||||
sql += ` LIMIT ${pageSize} OFFSET ${offset * 1 + page * pageSize};`;
|
||||
|
||||
log("$$$", sql);
|
||||
return sql;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export const translateQueryParams = (queries: any) => {
|
||||
}
|
||||
|
||||
// Fix invalid orderby when switching from spreadsheet layout
|
||||
if (layout === "spreadsheet" && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
|
||||
if (layout !== "spreadsheet" && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
|
||||
otherProps.order_by = "sort_order";
|
||||
}
|
||||
// For each property value, replace None with empty string
|
||||
@@ -336,6 +336,9 @@ const getSingleFilterFields = (queries: any) => {
|
||||
if (state_group) {
|
||||
fields.add("states.'group' as state_group");
|
||||
}
|
||||
if (order_by?.includes("estimate_point__key")) {
|
||||
fields.add("estimate_point");
|
||||
}
|
||||
return Array.from(fields);
|
||||
};
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
else this.loader = "mutation-loading";
|
||||
if (loadingType) this.loader = loadingType;
|
||||
|
||||
const status = this.inboxFilters?.status && uniq([...this.inboxFilters.status, EInboxIssueStatus.SNOOZED]);
|
||||
const status = this.inboxFilters?.status;
|
||||
const queryParams = this.inboxIssueQueryParams(
|
||||
{ ...this.inboxFilters, status },
|
||||
this.inboxSorting,
|
||||
|
||||
@@ -36,6 +36,7 @@ import { EIssueLayoutTypes, ISSUE_PRIORITIES } from "@/constants/issue";
|
||||
// helpers
|
||||
import { convertToISODateString } from "@/helpers/date-time.helper";
|
||||
// local-db
|
||||
import { SPECIAL_ORDER_BY } from "@/local-db/utils/query-constructor";
|
||||
import { updatePersistentLayer } from "@/local-db/utils/utils";
|
||||
// services
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
@@ -164,8 +165,8 @@ const ISSUE_ORDERBY_KEY: Record<TIssueOrderByOptions, keyof TIssue> = {
|
||||
"-issue_cycle__cycle__name": "cycle_id",
|
||||
target_date: "target_date",
|
||||
"-target_date": "target_date",
|
||||
estimate_point: "estimate_point",
|
||||
"-estimate_point": "estimate_point",
|
||||
estimate_point__key: "estimate_point",
|
||||
"-estimate_point__key": "estimate_point",
|
||||
start_date: "start_date",
|
||||
"-start_date": "start_date",
|
||||
link_count: "link_count",
|
||||
@@ -282,6 +283,19 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters;
|
||||
if (!displayFilters) return;
|
||||
|
||||
const layout = displayFilters.layout;
|
||||
const orderBy = displayFilters.order_by;
|
||||
|
||||
// Temporary code to fix no load order by
|
||||
if (
|
||||
this.rootIssueStore.rootStore.user.localDBEnabled &&
|
||||
layout !== EIssueLayoutTypes.SPREADSHEET &&
|
||||
orderBy &&
|
||||
Object.keys(SPECIAL_ORDER_BY).includes(orderBy)
|
||||
) {
|
||||
return "sort_order";
|
||||
}
|
||||
|
||||
return displayFilters?.order_by;
|
||||
}
|
||||
|
||||
@@ -1701,13 +1715,14 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
* @returns string | string[] of sortable fields to be used for sorting
|
||||
*/
|
||||
populateIssueDataForSorting(
|
||||
dataType: "state_id" | "label_ids" | "assignee_ids" | "module_ids" | "cycle_id",
|
||||
dataType: "state_id" | "label_ids" | "assignee_ids" | "module_ids" | "cycle_id" | "estimate_point",
|
||||
dataIds: string | string[] | null | undefined,
|
||||
projectId: string | undefined | null,
|
||||
order?: "asc" | "desc"
|
||||
) {
|
||||
if (!dataIds) return;
|
||||
|
||||
const dataValues: string[] = [];
|
||||
const dataValues: (string | number)[] = [];
|
||||
const isDataIdsArray = Array.isArray(dataIds);
|
||||
const dataIdsArray = isDataIdsArray ? dataIds : [dataIds];
|
||||
|
||||
@@ -1757,6 +1772,26 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "estimate_point": {
|
||||
// return if project Id does not exist
|
||||
if (!projectId) break;
|
||||
// get the estimate ID for the current Project
|
||||
const currentProjectEstimateId =
|
||||
this.rootIssueStore.rootStore.projectEstimate.currentActiveEstimateIdByProjectId(projectId);
|
||||
// return if current Estimate Id for the project is not available
|
||||
if (!currentProjectEstimateId) break;
|
||||
// get Estimate based on Id
|
||||
const estimate = this.rootIssueStore.rootStore.projectEstimate.estimateById(currentProjectEstimateId);
|
||||
// If Estimate is not available, then return
|
||||
if (!estimate) break;
|
||||
// Get Estimate Value
|
||||
const estimateKey = estimate?.estimatePointById(dataIds as string)?.key;
|
||||
|
||||
// If Value string i not available or empty then return
|
||||
if (estimateKey === undefined) break;
|
||||
|
||||
dataValues.push(estimateKey);
|
||||
}
|
||||
}
|
||||
|
||||
return isDataIdsArray ? (order ? orderBy(dataValues, undefined, [order]) : dataValues) : dataValues;
|
||||
@@ -1771,11 +1806,17 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(orderBy(array, "sort_order"));
|
||||
case "state__name":
|
||||
return getIssueIds(
|
||||
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]))
|
||||
orderBy(array, (issue) =>
|
||||
this.populateIssueDataForSorting("state_id", issue?.["state_id"], issue?.["project_id"])
|
||||
)
|
||||
);
|
||||
case "-state__name":
|
||||
return getIssueIds(
|
||||
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]), ["desc"])
|
||||
orderBy(
|
||||
array,
|
||||
(issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"], issue?.["project_id"]),
|
||||
["desc"]
|
||||
)
|
||||
);
|
||||
// dates
|
||||
case "created_at":
|
||||
@@ -1826,15 +1867,23 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
case "-attachment_count":
|
||||
return getIssueIds(orderBy(array, "attachment_count", ["desc"]));
|
||||
|
||||
case "estimate_point":
|
||||
case "estimate_point__key":
|
||||
return getIssueIds(
|
||||
orderBy(array, [getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"])
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "estimate_point"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("estimate_point", issue?.["estimate_point"], issue?.["project_id"]),
|
||||
])
|
||||
); //preferring sorting based on empty values to always keep the empty values below
|
||||
case "-estimate_point":
|
||||
case "-estimate_point__key":
|
||||
return getIssueIds(
|
||||
orderBy(
|
||||
array,
|
||||
[getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "estimate_point"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("estimate_point", issue?.["estimate_point"], issue?.["project_id"]),
|
||||
], //preferring sorting based on empty values to always keep the empty values below
|
||||
["asc", "desc"]
|
||||
)
|
||||
);
|
||||
@@ -1854,7 +1903,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], issue?.["project_id"], "asc"),
|
||||
])
|
||||
);
|
||||
case "-labels__name":
|
||||
@@ -1863,7 +1913,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
array,
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], issue?.["project_id"], "asc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
)
|
||||
@@ -1873,7 +1924,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], issue?.["project_id"], "asc"),
|
||||
])
|
||||
);
|
||||
case "-issue_module__module__name":
|
||||
@@ -1882,7 +1934,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
array,
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], issue?.["project_id"], "asc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
)
|
||||
@@ -1892,7 +1945,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "asc"),
|
||||
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], issue?.["project_id"], "asc"),
|
||||
])
|
||||
);
|
||||
case "-issue_cycle__cycle__name":
|
||||
@@ -1901,7 +1954,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
array,
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], issue?.["project_id"], "asc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
)
|
||||
@@ -1911,7 +1965,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], issue?.["project_id"], "asc"),
|
||||
])
|
||||
);
|
||||
case "-assignees__first_name":
|
||||
@@ -1920,7 +1975,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
array,
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], issue?.["project_id"], "asc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/components/instance";
|
||||
@@ -0,0 +1 @@
|
||||
export const MaintenanceMessage = () => <></>;
|
||||
@@ -1 +0,0 @@
|
||||
export * from "ce/components/maintenance-mode";
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
@@ -5647,9 +5647,9 @@ cross-fetch@^3.1.5:
|
||||
node-fetch "^2.6.12"
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
|
||||
integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
@@ -11287,7 +11287,16 @@ streamx@^2.15.0, streamx@^2.20.0:
|
||||
optionalDependencies:
|
||||
bare-events "^2.2.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -11375,7 +11384,14 @@ string_decoder@^1.1.1, string_decoder@^1.3.0:
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -11912,47 +11928,47 @@ tunnel-agent@^0.6.0:
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
turbo-darwin-64@2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.2.3.tgz#f0ced75ed031091e52851cbe8bb05d21a161a22b"
|
||||
integrity sha512-Rcm10CuMKQGcdIBS3R/9PMeuYnv6beYIHqfZFeKWVYEWH69sauj4INs83zKMTUiZJ3/hWGZ4jet9AOwhsssLyg==
|
||||
turbo-darwin-64@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.3.0.tgz#cf82cf4a816a267c65a71d2d3ec1baef5c6b0f78"
|
||||
integrity sha512-pji+D49PhFItyQjf2QVoLZw2d3oRGo8gJgKyOiRzvip78Rzie74quA8XNwSg/DuzM7xx6gJ3p2/LylTTlgZXxQ==
|
||||
|
||||
turbo-darwin-arm64@2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.2.3.tgz#0b4741383ab5070d8383891a65861a8869cc7202"
|
||||
integrity sha512-+EIMHkuLFqUdJYsA3roj66t9+9IciCajgj+DVek+QezEdOJKcRxlvDOS2BUaeN8kEzVSsNiAGnoysFWYw4K0HA==
|
||||
turbo-darwin-arm64@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.3.0.tgz#3e058a4e41130abce9df49a1fb5e271af85a1d99"
|
||||
integrity sha512-AJrGIL9BO41mwDF/IBHsNGwvtdyB911vp8f5mbNo1wG66gWTvOBg7WCtYQBvCo11XTenTfXPRSsAb7w3WAZb6w==
|
||||
|
||||
turbo-linux-64@2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.2.3.tgz#2b339db50c12bc52ce99139c156d5555717a209d"
|
||||
integrity sha512-UBhJCYnqtaeOBQLmLo8BAisWbc9v9daL9G8upLR+XGj6vuN/Nz6qUAhverN4Pyej1g4Nt1BhROnj6GLOPYyqxQ==
|
||||
turbo-linux-64@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.3.0.tgz#0aefff6047faed0ffdbf0980d5dd4f11ace51d65"
|
||||
integrity sha512-jZqW6vc2sPJT3M/3ZmV1Cg4ecQVPqsbHncG/RnogHpBu783KCSXIndgxvUQNm9qfgBYbZDBnP1md63O4UTElhw==
|
||||
|
||||
turbo-linux-arm64@2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.2.3.tgz#a4daf6e0872a4e2652e2d05d68ad18cee5b10e94"
|
||||
integrity sha512-hJYT9dN06XCQ3jBka/EWvvAETnHRs3xuO/rb5bESmDfG+d9yQjeTMlhRXKrr4eyIMt6cLDt1LBfyi+6CQ+VAwQ==
|
||||
turbo-linux-arm64@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.3.0.tgz#a00db7c7a88400cc0357bfeac2beb383a35e255e"
|
||||
integrity sha512-HUbDLJlvd/hxuyCNO0BmEWYQj0TugRMvSQeG8vHJH+Lq8qOgDAe7J0K73bFNbZejZQxW3C3XEiZFB3pnpO78+A==
|
||||
|
||||
turbo-windows-64@2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.2.3.tgz#d44b3385948bd0f2ef5c2d53391f142bdd467b18"
|
||||
integrity sha512-NPrjacrZypMBF31b4HE4ROg4P3nhMBPHKS5WTpMwf7wydZ8uvdEHpESVNMOtqhlp857zbnKYgP+yJF30H3N2dQ==
|
||||
turbo-windows-64@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.3.0.tgz#f082688f17c73d345efbdc43fb589b1df70cd53f"
|
||||
integrity sha512-c5rxrGNTYDWX9QeMzWLFE9frOXnKjHGEvQMp1SfldDlbZYsloX9UKs31TzUThzfTgTiz8NYuShaXJ2UvTMnV/g==
|
||||
|
||||
turbo-windows-arm64@2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.2.3.tgz#d0625ec53f467013a6f259f87f7fc4ae8670aaa4"
|
||||
integrity sha512-fnNrYBCqn6zgKPKLHu4sOkihBI/+0oYFr075duRxqUZ+1aLWTAGfHZLgjVeLh3zR37CVzuerGIPWAEkNhkWEIw==
|
||||
turbo-windows-arm64@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.3.0.tgz#42d77fe99f72b4862bb4cbbb0cb5dca73427270a"
|
||||
integrity sha512-7qfUuYhfIVb1AZgs89DxhXK+zZez6O2ocmixEQ4hXZK7ytnBt5vaz2zGNJJKFNYIL5HX1C3tuHolnpNgDNCUIg==
|
||||
|
||||
turbo@^2.1.1:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.2.3.tgz#0f45612d62526c98c75da0682aa8c26b902b5e07"
|
||||
integrity sha512-5lDvSqIxCYJ/BAd6rQGK/AzFRhBkbu4JHVMLmGh/hCb7U3CqSnr5Tjwfy9vc+/5wG2DJ6wttgAaA7MoCgvBKZQ==
|
||||
turbo@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.3.0.tgz#01e7841fafdd870564e1ad376b42dbc8a71d52b3"
|
||||
integrity sha512-/uOq5o2jwRPyaUDnwBpOR5k9mQq4c3wziBgWNWttiYQPmbhDtrKYPRBxTvA2WpgQwRIbt8UM612RMN8n/TvmHA==
|
||||
optionalDependencies:
|
||||
turbo-darwin-64 "2.2.3"
|
||||
turbo-darwin-arm64 "2.2.3"
|
||||
turbo-linux-64 "2.2.3"
|
||||
turbo-linux-arm64 "2.2.3"
|
||||
turbo-windows-64 "2.2.3"
|
||||
turbo-windows-arm64 "2.2.3"
|
||||
turbo-darwin-64 "2.3.0"
|
||||
turbo-darwin-arm64 "2.3.0"
|
||||
turbo-linux-64 "2.3.0"
|
||||
turbo-linux-arm64 "2.3.0"
|
||||
turbo-windows-64 "2.3.0"
|
||||
turbo-windows-arm64 "2.3.0"
|
||||
|
||||
tween-functions@^1.2.0:
|
||||
version "1.2.0"
|
||||
@@ -12607,7 +12623,16 @@ word-wrap@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
||||
Reference in New Issue
Block a user