Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f81e8ed84 | |||
| 39eabc28b5 | |||
| c92fe6191e | |||
| 7bb04003ea | |||
| 19dab1fad0 | |||
| 5f7b6ecf7f | |||
| dfd3af13cf | |||
| 4cc1b79d81 | |||
| 4a6f646317 | |||
| b8e21d92bf | |||
| b87d5c5be6 | |||
| ceda06e88d | |||
| eb344881c2 | |||
| 01257a6936 | |||
| 51b01ebcac | |||
| 0a8d66dcc3 | |||
| a5e3e4fe7d |
+1
-1
@@ -4,7 +4,7 @@ Thank you for showing an interest in contributing to Plane! All kinds of contrib
|
||||
|
||||
## Submitting an issue
|
||||
|
||||
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new informplaneation.
|
||||
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new information.
|
||||
|
||||
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.22.0"
|
||||
"version": "0.23.0"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
def get_api_token(self, request):
|
||||
return request.headers.get(self.auth_header_name)
|
||||
|
||||
def validate_api_token(self, token):
|
||||
def validate_api_token(self, token, slug):
|
||||
try:
|
||||
api_token = APIToken.objects.get(
|
||||
Q(
|
||||
@@ -30,6 +30,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
| Q(expired_at__isnull=True)
|
||||
),
|
||||
token=token,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
)
|
||||
except APIToken.DoesNotExist:
|
||||
@@ -41,10 +42,11 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
return (api_token.user, api_token.token)
|
||||
|
||||
def authenticate(self, request):
|
||||
slug = request.parser_context["kwargs"].get("slug", None)
|
||||
token = self.get_api_token(request=request)
|
||||
if not token:
|
||||
return None
|
||||
|
||||
# Validate the API token
|
||||
user, token = self.validate_api_token(token)
|
||||
user, token = self.validate_api_token(token, slug)
|
||||
return user, token
|
||||
|
||||
@@ -42,7 +42,7 @@ urlpatterns = [
|
||||
),
|
||||
# updated v2 paginated issues
|
||||
path(
|
||||
"workspaces/<str:slug>/v2/issues/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/v2/issues/",
|
||||
IssuePaginatedViewSet.as_view({"get": "list"}),
|
||||
name="project-issues-paginated",
|
||||
),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -19,6 +23,7 @@ from plane.app.serializers import (
|
||||
EstimateReadSerializer,
|
||||
)
|
||||
from plane.utils.cache import invalidate_cache
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
|
||||
def generate_random_name(length=10):
|
||||
@@ -249,11 +254,66 @@ class EstimatePointEndpoint(BaseViewSet):
|
||||
)
|
||||
# update all the issues with the new estimate
|
||||
if new_estimate_id:
|
||||
_ = Issue.objects.filter(
|
||||
issues = Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
estimate_point_id=estimate_point_id,
|
||||
).update(estimate_point_id=new_estimate_id)
|
||||
)
|
||||
for issue in issues:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(new_estimate_id)
|
||||
if new_estimate_id
|
||||
else None
|
||||
),
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=issue.id,
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(issue.estimate_point_id)
|
||||
if issue.estimate_point_id
|
||||
else None
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
issues.update(estimate_point_id=new_estimate_id)
|
||||
else:
|
||||
issues = Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
estimate_point_id=estimate_point_id,
|
||||
)
|
||||
for issue in issues:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"estimate_point": None,
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=issue.id,
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(issue.estimate_point_id)
|
||||
if issue.estimate_point_id
|
||||
else None
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
|
||||
# delete the estimate point
|
||||
old_estimate_point = EstimatePoint.objects.filter(
|
||||
|
||||
@@ -741,17 +741,12 @@ class DeletedIssuesListViewSet(BaseAPIView):
|
||||
class IssuePaginatedViewSet(BaseViewSet):
|
||||
def get_queryset(self):
|
||||
workspace_slug = self.kwargs.get("slug")
|
||||
|
||||
# getting the project_id from the request params
|
||||
project_id = self.request.GET.get("project_id", None)
|
||||
project_id = self.kwargs.get("project_id")
|
||||
|
||||
issue_queryset = Issue.issue_objects.filter(
|
||||
workspace__slug=workspace_slug
|
||||
workspace__slug=workspace_slug, project_id=project_id
|
||||
)
|
||||
|
||||
if project_id:
|
||||
issue_queryset = issue_queryset.filter(project_id=project_id)
|
||||
|
||||
return (
|
||||
issue_queryset.select_related(
|
||||
"workspace", "project", "state", "parent"
|
||||
@@ -793,8 +788,8 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
|
||||
return paginated_data
|
||||
|
||||
def list(self, request, slug):
|
||||
project_id = self.request.GET.get("project_id", None)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
cursor = request.GET.get("cursor", None)
|
||||
is_description_required = request.GET.get("description", False)
|
||||
updated_at = request.GET.get("updated_at__gt", None)
|
||||
@@ -833,14 +828,26 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
required_fields.append("description_html")
|
||||
|
||||
# querying issues
|
||||
base_queryset = Issue.issue_objects.filter(workspace__slug=slug)
|
||||
|
||||
if project_id:
|
||||
base_queryset = base_queryset.filter(project_id=project_id)
|
||||
base_queryset = Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
base_queryset = base_queryset.order_by("updated_at")
|
||||
queryset = self.get_queryset().order_by("updated_at")
|
||||
|
||||
# validation for guest user
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
project_member = ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
)
|
||||
if project_member.exists() and not project.guest_view_all_features:
|
||||
base_queryset = base_queryset.filter(created_by=request.user)
|
||||
queryset = queryset.filter(created_by=request.user)
|
||||
|
||||
# filtering issues by greater then updated_at given by the user
|
||||
if updated_at:
|
||||
base_queryset = base_queryset.filter(updated_at__gt=updated_at)
|
||||
|
||||
@@ -413,9 +413,20 @@ class ProjectViewSet(BaseViewSet):
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def partial_update(self, request, slug, pk=None):
|
||||
try:
|
||||
if not ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=pk,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "You don't have the required permissions."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
project = Project.objects.get(pk=pk)
|
||||
|
||||
@@ -74,7 +74,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
|
||||
# Get all active workspace members
|
||||
workspace_members = self.get_queryset()
|
||||
|
||||
if workspace_member.role > 5:
|
||||
serializer = WorkspaceMemberAdminSerializer(
|
||||
workspace_members,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from django.utils import timezone
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
# Third party imports
|
||||
@@ -18,17 +19,25 @@ def soft_delete_related_objects(
|
||||
for field in related_fields:
|
||||
if field.one_to_many or field.one_to_one:
|
||||
try:
|
||||
if field.one_to_many:
|
||||
related_objects = getattr(instance, field.name).all()
|
||||
elif field.one_to_one:
|
||||
related_object = getattr(instance, field.name)
|
||||
related_objects = (
|
||||
[related_object] if related_object is not None else []
|
||||
)
|
||||
for obj in related_objects:
|
||||
if obj:
|
||||
obj.deleted_at = timezone.now()
|
||||
obj.save(using=using)
|
||||
# Check if the field has CASCADE on delete
|
||||
if (
|
||||
hasattr(field.remote_field, "on_delete")
|
||||
and field.remote_field.on_delete == models.CASCADE
|
||||
):
|
||||
if field.one_to_many:
|
||||
related_objects = getattr(instance, field.name).all()
|
||||
elif field.one_to_one:
|
||||
related_object = getattr(instance, field.name)
|
||||
related_objects = (
|
||||
[related_object]
|
||||
if related_object is not None
|
||||
else []
|
||||
)
|
||||
|
||||
for obj in related_objects:
|
||||
if obj:
|
||||
obj.deleted_at = timezone.now()
|
||||
obj.save(using=using)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
@@ -154,8 +163,7 @@ def hard_delete():
|
||||
if hasattr(model, "deleted_at"):
|
||||
# Get all instances where 'deleted_at' is greater than 30 days ago
|
||||
_ = model.all_objects.filter(
|
||||
deleted_at__lt=timezone.now()
|
||||
- timezone.timedelta(days=days)
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
return
|
||||
|
||||
@@ -465,7 +465,7 @@ def track_estimate_points(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
verb="removed" if new_estimate is None else "updated",
|
||||
old_identifier=(
|
||||
current_instance.get("estimate_point")
|
||||
if current_instance.get("estimate_point") is not None
|
||||
@@ -1700,16 +1700,12 @@ def issue_activity(
|
||||
event=(
|
||||
"issue_comment"
|
||||
if activity.field == "comment"
|
||||
else "inbox_issue"
|
||||
if inbox
|
||||
else "issue"
|
||||
else "inbox_issue" if inbox else "issue"
|
||||
),
|
||||
event_id=(
|
||||
activity.issue_comment_id
|
||||
if activity.field == "comment"
|
||||
else inbox
|
||||
if inbox
|
||||
else activity.issue_id
|
||||
else inbox if inbox else activity.issue_id
|
||||
),
|
||||
verb=activity.verb,
|
||||
field=(
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
from opentelemetry import trace
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import Instance
|
||||
from plane.db.models import (
|
||||
User,
|
||||
Workspace,
|
||||
Project,
|
||||
Issue,
|
||||
Module,
|
||||
Cycle,
|
||||
CycleIssue,
|
||||
ModuleIssue,
|
||||
Page,
|
||||
WorkspaceMember,
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def instance_traces():
|
||||
# Get the tracer
|
||||
tracer = trace.get_tracer(__name__)
|
||||
|
||||
# Check if the instance is registered
|
||||
instance = Instance.objects.first()
|
||||
|
||||
# If instance is None then return
|
||||
if instance is None:
|
||||
return
|
||||
|
||||
if instance.is_telemetry_enabled:
|
||||
# Instance details
|
||||
with tracer.start_as_current_span("instance_details") as span:
|
||||
# Count of all models
|
||||
workspace_count = Workspace.objects.count()
|
||||
user_count = User.objects.count()
|
||||
project_count = Project.objects.count()
|
||||
issue_count = Issue.objects.count()
|
||||
module_count = Module.objects.count()
|
||||
cycle_count = Cycle.objects.count()
|
||||
cycle_issue_count = CycleIssue.objects.count()
|
||||
module_issue_count = ModuleIssue.objects.count()
|
||||
page_count = Page.objects.count()
|
||||
|
||||
# Set span attributes
|
||||
span.set_attribute("instance_id", instance.instance_id)
|
||||
span.set_attribute("instance_name", instance.instance_name)
|
||||
span.set_attribute("current_version", instance.current_version)
|
||||
span.set_attribute("latest_version", instance.latest_version)
|
||||
span.set_attribute(
|
||||
"is_telemetry_enabled", instance.is_telemetry_enabled
|
||||
)
|
||||
span.set_attribute("user_count", user_count)
|
||||
span.set_attribute("workspace_count", workspace_count)
|
||||
span.set_attribute("project_count", project_count)
|
||||
span.set_attribute("issue_count", issue_count)
|
||||
span.set_attribute("module_count", module_count)
|
||||
span.set_attribute("cycle_count", cycle_count)
|
||||
span.set_attribute("cycle_issue_count", cycle_issue_count)
|
||||
span.set_attribute("module_issue_count", module_issue_count)
|
||||
span.set_attribute("page_count", page_count)
|
||||
|
||||
# Workspace details
|
||||
for workspace in Workspace.objects.all():
|
||||
# Count of all models
|
||||
project_count = Project.objects.filter(workspace=workspace).count()
|
||||
issue_count = Issue.objects.filter(workspace=workspace).count()
|
||||
module_count = Module.objects.filter(workspace=workspace).count()
|
||||
cycle_count = Cycle.objects.filter(workspace=workspace).count()
|
||||
cycle_issue_count = CycleIssue.objects.filter(
|
||||
workspace=workspace
|
||||
).count()
|
||||
module_issue_count = ModuleIssue.objects.filter(
|
||||
workspace=workspace
|
||||
).count()
|
||||
page_count = Page.objects.filter(workspace=workspace).count()
|
||||
member_count = WorkspaceMember.objects.filter(
|
||||
workspace=workspace
|
||||
).count()
|
||||
|
||||
# Set span attributes
|
||||
with tracer.start_as_current_span("workspace_details") as span:
|
||||
span.set_attribute("instance_id", instance.instance_id)
|
||||
span.set_attribute("workspace_id", str(workspace.id))
|
||||
span.set_attribute("workspace_slug", workspace.slug)
|
||||
span.set_attribute("project_count", project_count)
|
||||
span.set_attribute("issue_count", issue_count)
|
||||
span.set_attribute("module_count", module_count)
|
||||
span.set_attribute("cycle_count", cycle_count)
|
||||
span.set_attribute("cycle_issue_count", cycle_issue_count)
|
||||
span.set_attribute("module_issue_count", module_issue_count)
|
||||
span.set_attribute("page_count", page_count)
|
||||
span.set_attribute("member_count", member_count)
|
||||
|
||||
return
|
||||
@@ -9,7 +9,10 @@ from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import Instance
|
||||
from plane.db.models import User
|
||||
from plane.db.models import (
|
||||
User,
|
||||
)
|
||||
from plane.license.bgtasks.tracer import instance_traces
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -21,16 +24,24 @@ class Command(BaseCommand):
|
||||
"machine_signature", type=str, help="Machine signature"
|
||||
)
|
||||
|
||||
def read_package_json(self):
|
||||
with open("package.json", "r") as file:
|
||||
# Load JSON content from the file
|
||||
data = json.load(file)
|
||||
|
||||
payload = {
|
||||
"instance_key": settings.INSTANCE_KEY,
|
||||
"version": data.get("version", 0.1),
|
||||
"user_count": User.objects.filter(is_bot=False).count(),
|
||||
}
|
||||
return payload
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Check if the instance is registered
|
||||
instance = Instance.objects.first()
|
||||
|
||||
# If instance is None then register this instance
|
||||
if instance is None:
|
||||
with open("package.json", "r") as file:
|
||||
# Load JSON content from the file
|
||||
data = json.load(file)
|
||||
|
||||
machine_signature = options.get(
|
||||
"machine_signature", "machine-signature"
|
||||
)
|
||||
@@ -38,12 +49,7 @@ class Command(BaseCommand):
|
||||
if not machine_signature:
|
||||
raise CommandError("Machine signature is required")
|
||||
|
||||
payload = {
|
||||
"instance_key": settings.INSTANCE_KEY,
|
||||
"version": data.get("version", 0.1),
|
||||
"machine_signature": machine_signature,
|
||||
"user_count": User.objects.filter(is_bot=False).count(),
|
||||
}
|
||||
payload = self.read_package_json()
|
||||
|
||||
instance = Instance.objects.create(
|
||||
instance_name="Plane Community Edition",
|
||||
@@ -60,4 +66,15 @@ class Command(BaseCommand):
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Instance already registered")
|
||||
)
|
||||
return
|
||||
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.save()
|
||||
|
||||
# Call the instance traces task
|
||||
instance_traces.delay()
|
||||
|
||||
return
|
||||
|
||||
@@ -16,6 +16,17 @@ from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from corsheaders.defaults import default_headers
|
||||
|
||||
# OpenTelemetry
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
||||
OTLPSpanExporter,
|
||||
)
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Secret Key
|
||||
@@ -24,6 +35,19 @@ 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})
|
||||
trace.set_tracer_provider(TracerProvider(resource=resource))
|
||||
# Configure the OTLP exporter
|
||||
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)
|
||||
|
||||
|
||||
# Allowed Hosts
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
@@ -279,6 +303,7 @@ CELERY_IMPORTS = (
|
||||
"plane.bgtasks.file_asset_task",
|
||||
"plane.bgtasks.email_notification_task",
|
||||
"plane.bgtasks.api_logs_task",
|
||||
"plane.license.bgtasks.tracer",
|
||||
# management tasks
|
||||
"plane.bgtasks.dummy_data_task",
|
||||
)
|
||||
|
||||
@@ -62,3 +62,8 @@ zxcvbn==4.4.28
|
||||
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
|
||||
@@ -280,7 +280,7 @@ function download() {
|
||||
function startServices() {
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --pull if_not_present --quiet-pull"
|
||||
|
||||
local migrator_container_id=$(docker ps --format "{{.ID}} {{.Names}}" | grep -E "${SERVICE_FOLDER}(-migrator|_migrator)" | awk '{print $1}')
|
||||
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
|
||||
if [ -n "$migrator_container_id" ]; then
|
||||
local idx=0
|
||||
while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
|
||||
@@ -308,7 +308,7 @@ function startServices() {
|
||||
fi
|
||||
fi
|
||||
|
||||
local api_container_id=$(docker ps --format "{{.ID}} {{.Names}}" | grep -E "${SERVICE_FOLDER}(-api|_api)" | awk '{print $1}')
|
||||
local api_container_id=$(docker container ls -q -f "name=$SERVICE_FOLDER-api")
|
||||
local idx2=0
|
||||
while ! docker logs $api_container_id 2>&1 | grep -m 1 -i "Application startup complete" | grep -q ".";
|
||||
do
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"description": "",
|
||||
"main": "./src/server.ts",
|
||||
"private": true,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"private": true,
|
||||
"main": "./index.ts"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -71,6 +71,17 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
const containerRect = useRef<DOMRect | null>(null);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
const updateAttributesSafely = useCallback(
|
||||
(attributes: Partial<ImageAttributes>, errorMessage: string) => {
|
||||
try {
|
||||
updateAttributes(attributes);
|
||||
} catch (error) {
|
||||
console.error(`${errorMessage}:`, error);
|
||||
}
|
||||
},
|
||||
[updateAttributes]
|
||||
);
|
||||
|
||||
const handleImageLoad = useCallback(() => {
|
||||
const img = imageRef.current;
|
||||
if (!img) return;
|
||||
@@ -105,17 +116,25 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
};
|
||||
|
||||
setSize(initialComputedSize);
|
||||
updateAttributes(initialComputedSize);
|
||||
updateAttributesSafely(
|
||||
initialComputedSize,
|
||||
"Failed to update attributes while initializing an image for the first time:"
|
||||
);
|
||||
} else {
|
||||
// as the aspect ratio in not stored for old images, we need to update the attrs
|
||||
setSize((prevSize) => {
|
||||
const newSize = { ...prevSize, aspectRatio };
|
||||
updateAttributes(newSize);
|
||||
return newSize;
|
||||
});
|
||||
if (!aspectRatio) {
|
||||
setSize((prevSize) => {
|
||||
const newSize = { ...prevSize, aspectRatio };
|
||||
updateAttributesSafely(
|
||||
newSize,
|
||||
"Failed to update attributes while initializing images with width but no aspect ratio:"
|
||||
);
|
||||
return newSize;
|
||||
});
|
||||
}
|
||||
}
|
||||
setInitialResizeComplete(true);
|
||||
}, [width, updateAttributes, editorContainer]);
|
||||
}, [width, updateAttributes, editorContainer, aspectRatio]);
|
||||
|
||||
// for real time resizing
|
||||
useLayoutEffect(() => {
|
||||
@@ -142,7 +161,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
|
||||
const handleResizeEnd = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
updateAttributes(size);
|
||||
updateAttributesSafely(size, "Failed to update attributes at the end of resizing:");
|
||||
}, [size, updateAttributes]);
|
||||
|
||||
const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {
|
||||
|
||||
@@ -5,9 +5,7 @@ import { ImageIcon } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useUploader, useDropZone } from "@/hooks/use-file-upload";
|
||||
// plugins
|
||||
import { isFileValid } from "@/plugins/image";
|
||||
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
// extensions
|
||||
import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image";
|
||||
|
||||
@@ -74,7 +72,11 @@ export const CustomImageUploader = (props: {
|
||||
);
|
||||
// hooks
|
||||
const { uploading: isImageBeingUploaded, uploadFile } = useUploader({ onUpload, editor, loadImageFromFileSystem });
|
||||
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ uploader: uploadFile });
|
||||
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
|
||||
uploader: uploadFile,
|
||||
editor,
|
||||
pos: getPos(),
|
||||
});
|
||||
|
||||
// the meta data of the image component
|
||||
const meta = useMemo(
|
||||
@@ -82,9 +84,6 @@ export const CustomImageUploader = (props: {
|
||||
[imageComponentImageFileMap, imageEntityId]
|
||||
);
|
||||
|
||||
// if the image component is dropped, we check if it has an existing file
|
||||
const existingFile = useMemo(() => (meta && meta.event === "drop" ? meta.file : undefined), [meta]);
|
||||
|
||||
// after the image component is mounted we start the upload process based on
|
||||
// it's uploaded
|
||||
useEffect(() => {
|
||||
@@ -100,27 +99,20 @@ export const CustomImageUploader = (props: {
|
||||
}
|
||||
}, [meta, uploadFile, imageComponentImageFileMap]);
|
||||
|
||||
// check if the image is dropped and set the local image as the existing file
|
||||
useEffect(() => {
|
||||
if (existingFile) {
|
||||
uploadFile(existingFile);
|
||||
}
|
||||
}, [existingFile, uploadFile]);
|
||||
|
||||
const onFileChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
if (isFileValid(file)) {
|
||||
uploadFile(file);
|
||||
}
|
||||
async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const fileList = e.target.files;
|
||||
if (!fileList) {
|
||||
return;
|
||||
}
|
||||
await uploadFirstImageAndInsertRemaining(editor, fileList, getPos(), uploadFile);
|
||||
},
|
||||
[uploadFile]
|
||||
[uploadFile, editor, getPos]
|
||||
);
|
||||
|
||||
const getDisplayMessage = useCallback(() => {
|
||||
const isUploading = isImageBeingUploaded || existingFile;
|
||||
const isUploading = isImageBeingUploaded;
|
||||
if (failedToLoadImage) {
|
||||
return "Error loading image";
|
||||
}
|
||||
@@ -134,13 +126,14 @@ export const CustomImageUploader = (props: {
|
||||
}
|
||||
|
||||
return "Add an image";
|
||||
}, [draggedInside, failedToLoadImage, existingFile, isImageBeingUploaded]);
|
||||
}, [draggedInside, failedToLoadImage, isImageBeingUploaded]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"image-upload-component flex items-center justify-start gap-2 py-3 px-2 rounded-lg text-custom-text-300 hover:text-custom-text-200 bg-custom-background-90 hover:bg-custom-background-80 border border-dashed border-custom-border-300 cursor-pointer transition-all duration-200 ease-in-out",
|
||||
"image-upload-component flex items-center justify-start gap-2 py-3 px-2 rounded-lg text-custom-text-300 hover:text-custom-text-200 bg-custom-background-90 hover:bg-custom-background-80 border border-dashed border-custom-border-300 transition-all duration-200 ease-in-out cursor-default",
|
||||
{
|
||||
"hover:text-custom-text-200 cursor-pointer": editor.isEditable,
|
||||
"bg-custom-background-80 text-custom-text-200": draggedInside,
|
||||
"text-custom-primary-200 bg-custom-primary-100/10 hover:bg-custom-primary-100/10 hover:text-custom-primary-200 border-custom-primary-200/10":
|
||||
selected,
|
||||
@@ -153,7 +146,7 @@ export const CustomImageUploader = (props: {
|
||||
onDragLeave={onDragLeave}
|
||||
contentEditable={false}
|
||||
onClick={() => {
|
||||
if (!failedToLoadImage) {
|
||||
if (!failedToLoadImage && editor.isEditable) {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
}}
|
||||
@@ -167,6 +160,7 @@ export const CustomImageUploader = (props: {
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.webp"
|
||||
onChange={onFileChange}
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ export const DropHandlerExtension = () =>
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
const pos = view.state.selection.from;
|
||||
insertImages({ editor, files: imageFiles, initialPos: pos, event: "drop" });
|
||||
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export const DropHandlerExtension = () =>
|
||||
|
||||
if (coordinates) {
|
||||
const pos = coordinates.pos;
|
||||
insertImages({ editor, files: imageFiles, initialPos: pos, event: "drop" });
|
||||
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export const DropHandlerExtension = () =>
|
||||
},
|
||||
});
|
||||
|
||||
const insertImages = async ({
|
||||
export const insertImagesSafely = async ({
|
||||
editor,
|
||||
files,
|
||||
initialPos,
|
||||
@@ -72,13 +72,6 @@ const insertImages = async ({
|
||||
const docSize = editor.state.doc.content.size;
|
||||
pos = Math.min(pos, docSize);
|
||||
|
||||
// Check if the position has a non-empty node
|
||||
const nodeAtPos = editor.state.doc.nodeAt(pos);
|
||||
if (nodeAtPos && nodeAtPos.content.size > 0) {
|
||||
// Move to the end of the current node
|
||||
pos += nodeAtPos.nodeSize;
|
||||
}
|
||||
|
||||
try {
|
||||
// Insert the image at the current position
|
||||
editor.commands.insertImageComponent({ file, pos, event });
|
||||
|
||||
@@ -126,7 +126,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
forwardedRef,
|
||||
() => ({
|
||||
clearEditor: (emitUpdate = false) => {
|
||||
editorRef.current?.commands.clearContent(emitUpdate);
|
||||
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DragEvent, useCallback, useEffect, useState } from "react";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { isFileValid } from "@/plugins/image";
|
||||
import { insertImagesSafely } from "@/extensions/drop";
|
||||
|
||||
export const useUploader = ({
|
||||
onUpload,
|
||||
@@ -63,7 +64,15 @@ export const useUploader = ({
|
||||
return { uploading, uploadFile };
|
||||
};
|
||||
|
||||
export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => {
|
||||
export const useDropZone = ({
|
||||
uploader,
|
||||
editor,
|
||||
pos,
|
||||
}: {
|
||||
uploader: (file: File) => Promise<void>;
|
||||
editor: Editor;
|
||||
pos: number;
|
||||
}) => {
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [draggedInside, setDraggedInside] = useState<boolean>(false);
|
||||
|
||||
@@ -86,40 +95,16 @@ export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) =>
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setDraggedInside(false);
|
||||
if (e.dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileList = e.dataTransfer.files;
|
||||
|
||||
const files: File[] = [];
|
||||
|
||||
for (let i = 0; i < fileList.length; i += 1) {
|
||||
const item = fileList.item(i);
|
||||
if (item) {
|
||||
files.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (files.some((file) => file.type.indexOf("image") === -1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const filteredFiles = files.filter((f) => f.type.indexOf("image") !== -1);
|
||||
|
||||
const file = filteredFiles.length > 0 ? filteredFiles[0] : undefined;
|
||||
|
||||
if (file) {
|
||||
uploader(file);
|
||||
} else {
|
||||
console.error("No file found");
|
||||
}
|
||||
await uploadFirstImageAndInsertRemaining(editor, fileList, pos, uploader);
|
||||
},
|
||||
[uploader]
|
||||
[uploader, editor, pos]
|
||||
);
|
||||
|
||||
const onDragEnter = () => {
|
||||
@@ -143,3 +128,40 @@ function trimFileName(fileName: string, maxLength = 100) {
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
// Upload the first image and insert the remaining images for uploading multiple image
|
||||
// post insertion of image-component
|
||||
export async function uploadFirstImageAndInsertRemaining(
|
||||
editor: Editor,
|
||||
fileList: FileList,
|
||||
pos: number,
|
||||
uploaderFn: (file: File) => Promise<void>
|
||||
) {
|
||||
const filteredFiles: File[] = [];
|
||||
for (let i = 0; i < fileList.length; i += 1) {
|
||||
const item = fileList.item(i);
|
||||
if (item && item.type.indexOf("image") !== -1 && isFileValid(item)) {
|
||||
filteredFiles.push(item);
|
||||
}
|
||||
}
|
||||
if (filteredFiles.length !== fileList.length) {
|
||||
console.warn("Some files were not images and have been ignored.");
|
||||
}
|
||||
if (filteredFiles.length === 0) {
|
||||
console.error("No image files found to upload");
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the first image
|
||||
const firstFile = filteredFiles[0];
|
||||
uploaderFn(firstFile);
|
||||
|
||||
// Insert the remaining images
|
||||
const remainingFiles = filteredFiles.slice(1);
|
||||
|
||||
if (remainingFiles.length > 0) {
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const posOfNextImageToBeInserted = Math.min(pos + 1, docSize);
|
||||
insertImagesSafely({ editor, files: remainingFiles, initialPos: posOfNextImageToBeInserted, event: "drop" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
clearEditor: () => {
|
||||
editorRef.current?.commands.clearContent();
|
||||
clearEditor: (emitUpdate = false) => {
|
||||
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
|
||||
@@ -17,6 +17,8 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag
|
||||
});
|
||||
|
||||
transactions.forEach((transaction) => {
|
||||
// if the transaction has meta of skipImageDeletion get to true, then return (like while clearing the editor content programatically)
|
||||
if (transaction.getMeta("skipImageDeletion")) return;
|
||||
// transaction could be a selection
|
||||
if (!transaction.docChanged) return;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@plane/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"files": [
|
||||
"library.js",
|
||||
"next.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/helpers",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"description": "Helper functions shared across multiple apps internally",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"private": true,
|
||||
"types": "./src/index.d.ts",
|
||||
"main": "./src/index.d.ts"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/typescript-config",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { Check, ChevronDown, Info, Search } from "lucide-react";
|
||||
import { createPortal } from "react-dom";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/helpers";
|
||||
@@ -11,6 +11,8 @@ import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
|
||||
import { cn } from "../../helpers";
|
||||
// types
|
||||
import { ICustomSearchSelectProps } from "./helper";
|
||||
// local components
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
const {
|
||||
@@ -95,11 +97,10 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex w-full items-center justify-between gap-1 text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
className={`flex w-full items-center justify-between gap-1 text-xs ${disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{customButton}
|
||||
@@ -170,17 +171,32 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
"w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none",
|
||||
{
|
||||
"bg-custom-background-80": active,
|
||||
"text-custom-text-400 opacity-60 cursor-not-allowed": option.disabled,
|
||||
}
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (!multiple) closeDropdown();
|
||||
}}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span className="flex-grow truncate">{option.content}</span>
|
||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||
{option.tooltip && (
|
||||
<>
|
||||
{
|
||||
typeof option.tooltip === "string" ? (
|
||||
<Tooltip tooltipContent={option.tooltip}>
|
||||
<Info className="h-3.5 w-3.5 flex-shrink-0 cursor-pointer text-custom-text-200" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
option.tooltip
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
|
||||
@@ -44,12 +44,14 @@ interface CustomSearchSelectProps {
|
||||
onChange: any;
|
||||
onClose?: () => void;
|
||||
options:
|
||||
| {
|
||||
value: any;
|
||||
query: string;
|
||||
content: React.ReactNode;
|
||||
}[]
|
||||
| undefined;
|
||||
| {
|
||||
value: any;
|
||||
query: string;
|
||||
content: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | React.ReactNode;
|
||||
}[]
|
||||
| undefined;
|
||||
}
|
||||
|
||||
interface SingleValueProps {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { Control } from "react-hook-form";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { TBulkIssueProperties, TIssue } from "@plane/types";
|
||||
|
||||
type TIssueTypeSelectProps = {
|
||||
control: Control<TIssue>;
|
||||
export type TIssueFields = TIssue & TBulkIssueProperties;
|
||||
|
||||
export type TIssueTypeDropdownVariant = "xs" | "sm";
|
||||
|
||||
export type TIssueTypeSelectProps<T extends Partial<TIssueFields>> = {
|
||||
control: Control<T>;
|
||||
projectId: string | null;
|
||||
disabled?: boolean;
|
||||
handleFormChange: () => void;
|
||||
variant?: TIssueTypeDropdownVariant;
|
||||
placeholder?: string;
|
||||
isRequired?: boolean;
|
||||
renderChevron?: boolean;
|
||||
dropDownContainerClassName?: string;
|
||||
showMandatoryFieldInfo?: boolean; // Show info about mandatory fields
|
||||
handleFormChange?: () => void;
|
||||
};
|
||||
|
||||
export const IssueTypeSelect: React.FC<TIssueTypeSelectProps> = () => <></>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const IssueTypeSelect = <T extends Partial<TIssueFields>>(props: TIssueTypeSelectProps<T>) => <></>;
|
||||
|
||||
@@ -280,6 +280,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
projectId={projectId}
|
||||
disabled={!!data?.sourceIssueId}
|
||||
handleFormChange={handleFormChange}
|
||||
renderChevron
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -182,7 +182,7 @@ export const SidebarFavoritesMenu = observer(() => {
|
||||
.map((fav, index) => (
|
||||
<Tooltip
|
||||
key={fav.id}
|
||||
tooltipContent={fav.entity_data ? fav.entity_data.name : fav.name}
|
||||
tooltipContent={fav?.entity_data ? fav.entity_data?.name : fav?.name}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapsed}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useProject, usePage, useProjectView, useCycle, useModule } from "@/hook
|
||||
export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorite) => {
|
||||
const favoriteItemId = favorite?.entity_data?.id;
|
||||
const favoriteItemLogoProps = favorite?.entity_data?.logo_props;
|
||||
const favoriteItemName = favorite?.entity_data.name || favorite?.name;
|
||||
const favoriteItemName = favorite?.entity_data?.name || favorite?.name;
|
||||
const favoriteItemEntityType = favorite?.entity_type;
|
||||
|
||||
// store hooks
|
||||
|
||||
@@ -55,14 +55,7 @@ export class IssueService extends APIService {
|
||||
queries?: any,
|
||||
config = {}
|
||||
): Promise<TIssuesResponse> {
|
||||
queries.project_id = projectId;
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/v2/issues/`,
|
||||
{
|
||||
params: queries,
|
||||
},
|
||||
config
|
||||
)
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/v2/issues/`, { params: queries }, config)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
Reference in New Issue
Block a user