Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c376aaf0a | |||
| 24899887b2 | |||
| c6953ff878 | |||
| 06be9ab81b | |||
| ed8d00acb1 | |||
| 915e374485 | |||
| 1d5b93cebd | |||
| df65b8c34a | |||
| 4c688b1d25 | |||
| bfc6ed839f | |||
| b68396a4b2 | |||
| b4fc715aba | |||
| 33a1b916cb | |||
| 2818310619 | |||
| 882520b3c7 | |||
| 20132e7544 | |||
| 0ae57b49d2 | |||
| d347269afb | |||
| a3fd616ec4 | |||
| 9eeff158d5 | |||
| ef20b5814e | |||
| 14914e8716 | |||
| b738e39a4a | |||
| 993c7899b6 | |||
| 2b411de1e3 | |||
| 1f9222065e | |||
| 670134562f | |||
| 144c793e9e | |||
| 0a924e4824 | |||
| 08702a5381 | |||
| 270f282c3c | |||
| 37699362ad | |||
| 27cec64c56 | |||
| 782b09eeaf | |||
| 5ac5892fe5 | |||
| 96c403ff0b | |||
| 543552f492 | |||
| c3cfcc1b92 | |||
| ac84d6ecf0 | |||
| 475b7a8396 | |||
| 00f78bd6a1 | |||
| 34337f90c1 | |||
| 4f68aaafa6 | |||
| 9c10235fca | |||
| 9c1b158291 | |||
| 2d0a15efd6 |
@@ -39,7 +39,15 @@ class CycleSerializer(BaseSerializer):
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("end_date", None) is not None
|
||||
):
|
||||
project_id = self.initial_data.get("project_id") or self.instance.project_id
|
||||
project_id = self.initial_data.get("project_id") or (
|
||||
self.instance.project_id
|
||||
if self.instance and hasattr(self.instance, "project_id")
|
||||
else None
|
||||
)
|
||||
|
||||
if not project_id:
|
||||
raise serializers.ValidationError("Project ID is required")
|
||||
|
||||
is_start_date_end_date_equal = (
|
||||
True
|
||||
if str(data.get("start_date")) == str(data.get("end_date"))
|
||||
|
||||
@@ -16,7 +16,6 @@ class ProjectSerializer(BaseSerializer):
|
||||
member_role = serializers.IntegerField(read_only=True)
|
||||
is_deployed = serializers.BooleanField(read_only=True)
|
||||
cover_image_url = serializers.CharField(read_only=True)
|
||||
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
|
||||
@@ -4,16 +4,6 @@ from plane.api.views import IntakeIssueAPIEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
|
||||
@@ -141,8 +141,10 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
if pk:
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
|
||||
data = CycleSerializer(
|
||||
queryset, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
queryset,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
).data
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||
@@ -154,8 +156,11 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
start_date__lte=timezone.now(), end_date__gte=timezone.now()
|
||||
)
|
||||
data = CycleSerializer(
|
||||
queryset, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
queryset,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
).data
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -166,8 +171,11 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -178,8 +186,11 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -190,8 +201,11 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -204,16 +218,22 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
).data,
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
).data,
|
||||
)
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ from plane.settings.storage import S3Storage
|
||||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
|
||||
|
||||
class WorkspaceIssueAPIEndpoint(BaseAPIView):
|
||||
@@ -322,6 +323,17 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
|
||||
# Send the model activity
|
||||
model_activity.delay(
|
||||
model_name="issue",
|
||||
model_id=str(serializer.data["id"]),
|
||||
requested_data=request.data,
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ProjectAPIEndpoint(BaseAPIView):
|
||||
"""Project Endpoints to create, update, list, retrieve and delete endpoint"""
|
||||
|
||||
@@ -238,7 +239,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
@@ -247,7 +248,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
def patch(self, request, slug, pk):
|
||||
@@ -258,9 +259,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
intake_view = request.data.get(
|
||||
"inbox_view", request.data.get("intake_view", project.intake_view)
|
||||
)
|
||||
intake_view = request.data.get("intake_view", project.intake_view)
|
||||
|
||||
if project.archived_at:
|
||||
return Response(
|
||||
@@ -307,7 +306,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||
return Response(
|
||||
@@ -316,7 +315,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
def delete(self, request, slug, pk):
|
||||
|
||||
@@ -352,8 +352,19 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
@@ -383,8 +394,19 @@ class RelatedIssueSerializer(BaseSerializer):
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
class IssueAssigneeSerializer(BaseSerializer):
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.app.views import IntakeViewSet, IntakeIssueViewSet
|
||||
from plane.app.views import (
|
||||
IntakeViewSet,
|
||||
IntakeIssueViewSet,
|
||||
IntakeWorkItemDescriptionVersionEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -53,4 +57,14 @@ urlpatterns = [
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-work-items/<uuid:work_item_id>/description-versions/",
|
||||
IntakeWorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="intake-work-item-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-work-items/<uuid:work_item_id>/description-versions/<uuid:pk>/",
|
||||
IntakeWorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="intake-work-item-versions",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -25,7 +25,7 @@ from plane.app.views import (
|
||||
IssueAttachmentV2Endpoint,
|
||||
IssueBulkUpdateDateEndpoint,
|
||||
IssueVersionEndpoint,
|
||||
IssueDescriptionVersionEndpoint,
|
||||
WorkItemDescriptionVersionEndpoint,
|
||||
IssueMetaEndpoint,
|
||||
IssueDetailIdentifierEndpoint,
|
||||
)
|
||||
@@ -263,22 +263,22 @@ urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
name="issue-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
name="issue-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
|
||||
IssueDescriptionVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:work_item_id>/description-versions/",
|
||||
WorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="work-item-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
|
||||
IssueDescriptionVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:work_item_id>/description-versions/<uuid:pk>/",
|
||||
WorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="work-item-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/meta/",
|
||||
|
||||
@@ -144,7 +144,7 @@ from .issue.sub_issue import SubIssuesEndpoint
|
||||
|
||||
from .issue.subscriber import IssueSubscriberViewSet
|
||||
|
||||
from .issue.version import IssueVersionEndpoint, IssueDescriptionVersionEndpoint
|
||||
from .issue.version import IssueVersionEndpoint, WorkItemDescriptionVersionEndpoint
|
||||
|
||||
from .module.base import (
|
||||
ModuleViewSet,
|
||||
@@ -184,7 +184,11 @@ from .estimate.base import (
|
||||
EstimatePointEndpoint,
|
||||
)
|
||||
|
||||
from .intake.base import IntakeViewSet, IntakeIssueViewSet
|
||||
from .intake.base import (
|
||||
IntakeViewSet,
|
||||
IntakeIssueViewSet,
|
||||
IntakeWorkItemDescriptionVersionEndpoint,
|
||||
)
|
||||
|
||||
from .analytic.base import (
|
||||
AnalyticsEndpoint,
|
||||
|
||||
@@ -27,16 +27,22 @@ from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
IssueDescriptionVersion,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
IssueSerializer,
|
||||
IssueDetailSerializer,
|
||||
IntakeSerializer,
|
||||
IntakeIssueSerializer,
|
||||
IntakeIssueDetailSerializer,
|
||||
IssueDescriptionVersionDetailSerializer,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.bgtasks.issue_description_version_task import issue_description_version_task
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from plane.utils.global_paginator import paginate
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
@@ -88,7 +94,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
serializer_class = IntakeIssueSerializer
|
||||
model = IntakeIssue
|
||||
|
||||
filterset_fields = ["statulls"]
|
||||
filterset_fields = ["status"]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
@@ -219,7 +225,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
@@ -287,6 +293,13 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
origin=base_host(request=request, is_app=True),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
updated_issue=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
issue_id=str(serializer.data["id"]),
|
||||
user_id=request.user.id,
|
||||
is_creating=True,
|
||||
)
|
||||
intake_issue = (
|
||||
IntakeIssue.objects.select_related("issue")
|
||||
.prefetch_related("issue__labels", "issue__assignees")
|
||||
@@ -386,13 +399,16 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
),
|
||||
"description": issue_data.get("description", issue.description),
|
||||
}
|
||||
current_instance = json.dumps(
|
||||
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
issue_serializer = IssueCreateSerializer(
|
||||
issue, data=issue_data, partial=True, context={"project_id": project_id}
|
||||
)
|
||||
|
||||
if issue_serializer.is_valid():
|
||||
current_instance = issue
|
||||
|
||||
# Log all the updates
|
||||
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||
if issue is not None:
|
||||
@@ -402,15 +418,18 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
updated_issue=current_instance,
|
||||
issue_id=str(pk),
|
||||
user_id=request.user.id,
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
return Response(
|
||||
@@ -550,7 +569,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
@@ -558,7 +577,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue = IntakeIssueDetailSerializer(intake_issue).data
|
||||
return Response(issue, status=status.HTTP_200_OK)
|
||||
@@ -585,3 +604,81 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
|
||||
intake_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class IntakeWorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
|
||||
def process_paginated_result(self, fields, results, timezone):
|
||||
paginated_data = results.values(*fields)
|
||||
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
paginated_data = user_timezone_converter(
|
||||
paginated_data, datetime_fields, timezone
|
||||
)
|
||||
|
||||
return paginated_data
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, work_item_id, pk=None):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=work_item_id
|
||||
)
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue.created_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
if pk:
|
||||
issue_description_version = IssueDescriptionVersion.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=work_item_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
serializer = IssueDescriptionVersionDetailSerializer(
|
||||
issue_description_version
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
cursor = request.GET.get("cursor", None)
|
||||
|
||||
required_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"last_saved_at",
|
||||
"owned_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
|
||||
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=work_item_id
|
||||
)
|
||||
|
||||
paginated_data = paginate(
|
||||
base_queryset=issue_description_versions_queryset,
|
||||
queryset=issue_description_versions_queryset,
|
||||
cursor=cursor,
|
||||
on_result=lambda results: self.process_paginated_result(
|
||||
required_fields, results, request.user.user_timezone
|
||||
),
|
||||
)
|
||||
return Response(paginated_data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -63,6 +63,7 @@ from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.bgtasks.issue_description_version_task import issue_description_version_task
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueListEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id):
|
||||
@@ -565,7 +566,7 @@ class IssueViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
recent_visited_task.delay(
|
||||
@@ -632,7 +633,7 @@ class IssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
@@ -1034,9 +1035,17 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
|
||||
"""
|
||||
Validate that start date is before target date.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
start = new_start or current_start
|
||||
target = new_target or current_target
|
||||
|
||||
# Convert string dates to datetime objects if they're strings
|
||||
if isinstance(start, str):
|
||||
start = datetime.strptime(start, "%Y-%m-%d").date()
|
||||
if isinstance(target, str):
|
||||
target = datetime.strptime(target, "%Y-%m-%d").date()
|
||||
|
||||
if start and target and start > target:
|
||||
return False
|
||||
return True
|
||||
@@ -1278,7 +1287,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
recent_visited_task.delay(
|
||||
|
||||
@@ -3,7 +3,13 @@ from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import IssueVersion, IssueDescriptionVersion
|
||||
from plane.db.models import (
|
||||
IssueVersion,
|
||||
IssueDescriptionVersion,
|
||||
Project,
|
||||
ProjectMember,
|
||||
Issue,
|
||||
)
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
IssueVersionDetailSerializer,
|
||||
@@ -66,7 +72,7 @@ class IssueVersionEndpoint(BaseAPIView):
|
||||
return Response(paginated_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueDescriptionVersionEndpoint(BaseAPIView):
|
||||
class WorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
def process_paginated_result(self, fields, results, timezone):
|
||||
paginated_data = results.values(*fields)
|
||||
|
||||
@@ -78,10 +84,34 @@ class IssueDescriptionVersionEndpoint(BaseAPIView):
|
||||
return paginated_data
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, issue_id, pk=None):
|
||||
def get(self, request, slug, project_id, work_item_id, pk=None):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=work_item_id
|
||||
)
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue.created_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
if pk:
|
||||
issue_description_version = IssueDescriptionVersion.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=work_item_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
serializer = IssueDescriptionVersionDetailSerializer(
|
||||
@@ -105,8 +135,8 @@ class IssueDescriptionVersionEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id
|
||||
)
|
||||
workspace__slug=slug, project_id=project_id, issue_id=work_item_id
|
||||
).order_by("-created_at")
|
||||
paginated_data = paginate(
|
||||
base_queryset=issue_description_versions_queryset,
|
||||
queryset=issue_description_versions_queryset,
|
||||
|
||||
@@ -41,6 +41,7 @@ from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ProjectViewSet(BaseViewSet):
|
||||
serializer_class = ProjectListSerializer
|
||||
model = Project
|
||||
@@ -341,7 +342,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
@@ -350,7 +351,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
except serializers.ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, pk=None):
|
||||
@@ -419,7 +420,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||
return Response(
|
||||
@@ -428,7 +429,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
except serializers.ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, pk):
|
||||
|
||||
@@ -432,7 +432,7 @@ class IssueViewViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
serializer = IssueViewSerializer(issue_view)
|
||||
|
||||
@@ -29,7 +29,7 @@ class WebhookEndpoint(BaseAPIView):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"error": "URL already exists for the workspace"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
raise IntegrityError
|
||||
|
||||
|
||||
@@ -119,7 +119,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Get total members and role
|
||||
total_members=WorkspaceMember.objects.filter(workspace_id=serializer.data["id"]).count()
|
||||
total_members = WorkspaceMember.objects.filter(
|
||||
workspace_id=serializer.data["id"]
|
||||
).count()
|
||||
data = serializer.data
|
||||
data["total_members"] = total_members
|
||||
data["role"] = 20
|
||||
@@ -134,7 +136,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"slug": "The workspace with the slug already exists"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
@@ -167,10 +169,9 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
|
||||
role = (
|
||||
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True)
|
||||
.values("role")
|
||||
)
|
||||
role = WorkspaceMember.objects.filter(
|
||||
workspace=OuterRef("id"), member=request.user, is_active=True
|
||||
).values("role")
|
||||
|
||||
workspace = (
|
||||
Workspace.objects.prefetch_related(
|
||||
|
||||
@@ -32,7 +32,7 @@ from plane.settings.redis import redis_instance
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.bgtasks.webhook_task import webhook_activity
|
||||
from plane.utils.issue_relation_mapper import get_inverse_relation
|
||||
from plane.utils.valid_uuid import is_valid_uuid
|
||||
from plane.utils.uuid import is_valid_uuid
|
||||
|
||||
|
||||
# Track Changes in name
|
||||
@@ -307,6 +307,10 @@ def track_labels(
|
||||
|
||||
# Set of newly added labels
|
||||
for added_label in added_labels:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(added_label):
|
||||
continue
|
||||
|
||||
label = Label.objects.get(pk=added_label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -327,6 +331,10 @@ def track_labels(
|
||||
|
||||
# Set of dropped labels
|
||||
for dropped_label in dropped_labels:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(dropped_label):
|
||||
continue
|
||||
|
||||
label = Label.objects.get(pk=dropped_label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -373,6 +381,10 @@ def track_assignees(
|
||||
|
||||
bulk_subscribers = []
|
||||
for added_asignee in added_assignees:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(added_asignee):
|
||||
continue
|
||||
|
||||
assignee = User.objects.get(pk=added_asignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -406,6 +418,10 @@ def track_assignees(
|
||||
)
|
||||
|
||||
for dropped_assignee in dropped_assginees:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(dropped_assignee):
|
||||
continue
|
||||
|
||||
assignee = User.objects.get(pk=dropped_assignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Django imports
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.models import Max
|
||||
from django.db import connection, transaction
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Project, Issue, IssueSequence
|
||||
from plane.utils.uuid import convert_uuid_to_integer
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Fix duplicate sequences"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Positional argument
|
||||
parser.add_argument("issue_identifier", type=str, help="Issue Identifier")
|
||||
|
||||
def strict_str_to_int(self, s):
|
||||
if not s.isdigit() and not (s.startswith("-") and s[1:].isdigit()):
|
||||
raise ValueError("Invalid integer string")
|
||||
return int(s)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
workspace_slug = input("Workspace slug: ")
|
||||
|
||||
if not workspace_slug:
|
||||
raise CommandError("Workspace slug is required")
|
||||
|
||||
issue_identifier = options.get("issue_identifier", False)
|
||||
|
||||
# Validate issue_identifier
|
||||
if not issue_identifier:
|
||||
raise CommandError("Issue identifier is required")
|
||||
|
||||
# Validate issue identifier
|
||||
try:
|
||||
identifier = issue_identifier.split("-")
|
||||
|
||||
if len(identifier) != 2:
|
||||
raise ValueError("Invalid issue identifier format")
|
||||
|
||||
project_identifier = identifier[0]
|
||||
issue_sequence = self.strict_str_to_int(identifier[1])
|
||||
|
||||
# Fetch the project
|
||||
project = Project.objects.get(
|
||||
identifier__iexact=project_identifier, workspace__slug=workspace_slug
|
||||
)
|
||||
|
||||
# Get the issues
|
||||
issues = Issue.objects.filter(project=project, sequence_id=issue_sequence)
|
||||
# Check if there are duplicate issues
|
||||
if not issues.count() > 1:
|
||||
raise CommandError(
|
||||
"No duplicate issues found with the given identifier"
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"{issues.count()} issues found with identifier {issue_identifier}"
|
||||
)
|
||||
)
|
||||
with transaction.atomic():
|
||||
# This ensures only one transaction per project can execute this code at a time
|
||||
lock_key = convert_uuid_to_integer(project.id)
|
||||
|
||||
# Acquire an exclusive lock using the project ID as the lock key
|
||||
with connection.cursor() as cursor:
|
||||
# Get an exclusive lock using the project ID as the lock key
|
||||
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key])
|
||||
|
||||
# Get the maximum sequence ID for the project
|
||||
last_sequence = IssueSequence.objects.filter(project=project).aggregate(
|
||||
largest=Max("sequence")
|
||||
)["largest"]
|
||||
|
||||
bulk_issues = []
|
||||
bulk_issue_sequences = []
|
||||
|
||||
issue_sequence_map = {
|
||||
isq.issue_id: isq
|
||||
for isq in IssueSequence.objects.filter(project=project)
|
||||
}
|
||||
|
||||
# change the ids of duplicate issues
|
||||
for index, issue in enumerate(issues[1:]):
|
||||
updated_sequence_id = last_sequence + index + 1
|
||||
issue.sequence_id = updated_sequence_id
|
||||
bulk_issues.append(issue)
|
||||
|
||||
# Find the same issue sequence instance from the above queryset
|
||||
sequence_identifier = issue_sequence_map.get(issue.id)
|
||||
if sequence_identifier:
|
||||
sequence_identifier.sequence = updated_sequence_id
|
||||
bulk_issue_sequences.append(sequence_identifier)
|
||||
|
||||
Issue.objects.bulk_update(bulk_issues, ["sequence_id"])
|
||||
IssueSequence.objects.bulk_update(bulk_issue_sequences, ["sequence"])
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Sequence IDs updated successfully"))
|
||||
except Exception as e:
|
||||
raise CommandError(str(e))
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.2.17 on 2025-03-04 19:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0092_alter_deprecateddashboardwidget_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="moved_to_page",
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="moved_to_project",
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="pageversion",
|
||||
name="sub_pages_data",
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
]
|
||||
@@ -82,4 +82,4 @@ from .label import Label
|
||||
|
||||
from .device import Device, DeviceSession
|
||||
|
||||
from .sticky import Sticky
|
||||
from .sticky import Sticky
|
||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.db import models, transaction, connection
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
from django import apps
|
||||
@@ -15,8 +15,8 @@ from django import apps
|
||||
from plane.utils.html_processor import strip_tags
|
||||
from plane.db.mixins import SoftDeletionManager
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from .base import BaseModel
|
||||
from .project import ProjectBaseModel
|
||||
from plane.utils.uuid import convert_uuid_to_integer
|
||||
|
||||
|
||||
def get_default_properties():
|
||||
@@ -209,11 +209,18 @@ class Issue(ProjectBaseModel):
|
||||
|
||||
if self._state.adding:
|
||||
with transaction.atomic():
|
||||
last_sequence = (
|
||||
IssueSequence.objects.filter(project=self.project)
|
||||
.select_for_update()
|
||||
.aggregate(largest=models.Max("sequence"))["largest"]
|
||||
)
|
||||
# Create a lock for this specific project using an advisory lock
|
||||
# This ensures only one transaction per project can execute this code at a time
|
||||
lock_key = convert_uuid_to_integer(self.project.id)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
# Get an exclusive lock using the project ID as the lock key
|
||||
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key])
|
||||
|
||||
# Get the last sequence for the project
|
||||
last_sequence = IssueSequence.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(largest=models.Max("sequence"))["largest"]
|
||||
self.sequence_id = last_sequence + 1 if last_sequence else 1
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
|
||||
@@ -50,6 +50,8 @@ class Page(BaseModel):
|
||||
projects = models.ManyToManyField(
|
||||
"db.Project", related_name="pages", through="db.ProjectPage"
|
||||
)
|
||||
moved_to_page = models.UUIDField(null=True, blank=True)
|
||||
moved_to_project = models.UUIDField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Page"
|
||||
@@ -172,6 +174,7 @@ class PageVersion(BaseModel):
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
description_json = models.JSONField(default=dict, blank=True)
|
||||
sub_pages_data = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Page Version"
|
||||
|
||||
@@ -109,5 +109,5 @@ class InstanceWorkSpaceEndpoint(BaseAPIView):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"slug": "The workspace with the slug already exists"},
|
||||
status=status.HTTP_410_GONE,
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Python imports
|
||||
import logging
|
||||
import time
|
||||
|
||||
# Django imports
|
||||
from django.http import HttpRequest
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.request import Request
|
||||
|
||||
# Module imports
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
api_logger = logging.getLogger("plane.api")
|
||||
|
||||
|
||||
class RequestLoggerMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def _should_log_route(self, request: Request | HttpRequest) -> bool:
|
||||
"""
|
||||
Determines whether a route should be logged based on the request and status code.
|
||||
"""
|
||||
# Don't log health checks
|
||||
if request.path == "/" and request.method == "GET":
|
||||
return False
|
||||
return True
|
||||
|
||||
def __call__(self, request):
|
||||
# get the start time
|
||||
start_time = time.time()
|
||||
|
||||
# Get the response
|
||||
response = self.get_response(request)
|
||||
|
||||
# calculate the duration
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Check if logging is required
|
||||
log_true = self._should_log_route(request=request)
|
||||
|
||||
# If logging is not required, return the response
|
||||
if not log_true:
|
||||
return response
|
||||
|
||||
user_id = (
|
||||
request.user.id
|
||||
if getattr(request, "user")
|
||||
and getattr(request.user, "is_authenticated", False)
|
||||
else None
|
||||
)
|
||||
|
||||
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
||||
|
||||
# Log the request information
|
||||
api_logger.info(
|
||||
f"{request.method} {request.get_full_path()} {response.status_code}",
|
||||
extra={
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"status_code": response.status_code,
|
||||
"duration_ms": int(duration * 1000),
|
||||
"remote_addr": get_client_ip(request),
|
||||
"user_agent": user_agent,
|
||||
"user_id": user_id,
|
||||
},
|
||||
)
|
||||
|
||||
# return the response
|
||||
return response
|
||||
@@ -59,6 +59,7 @@ MIDDLEWARE = [
|
||||
"crum.CurrentRequestUserMiddleware",
|
||||
"django.middleware.gzip.GZipMiddleware",
|
||||
"plane.middleware.api_log_middleware.APITokenLogMiddleware",
|
||||
"plane.middleware.logger.RequestLoggerMiddleware",
|
||||
]
|
||||
|
||||
# Rest Framework settings
|
||||
|
||||
@@ -37,26 +37,31 @@ if not os.path.exists(LOG_DIR):
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"disable_existing_loggers": True,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
||||
"fmt": "%(levelname)s %(asctime)s %(module)s %(name)s %(message)s",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
"formatter": "json",
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"django.request": {
|
||||
"plane.api": {"level": "INFO", "handlers": ["console"], "propagate": False},
|
||||
"plane.worker": {"level": "INFO", "handlers": ["console"], "propagate": False},
|
||||
"plane.exception": {
|
||||
"level": "ERROR",
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"plane": {"handlers": ["console"], "level": "DEBUG", "propagate": False},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,11 +26,10 @@ if not os.path.exists(LOG_DIR):
|
||||
# Logging configuration
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"disable_existing_loggers": True,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
"format": "%(asctime)s [%(process)d] %(levelname)s %(name)s: %(message)s"
|
||||
},
|
||||
"json": {
|
||||
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
||||
@@ -40,7 +39,7 @@ LOGGING = {
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
"formatter": "json",
|
||||
"level": "INFO",
|
||||
},
|
||||
"file": {
|
||||
@@ -59,15 +58,19 @@ LOGGING = {
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django": {"handlers": ["console", "file"], "level": "INFO", "propagate": True},
|
||||
"django.request": {
|
||||
"handlers": ["console", "file"],
|
||||
"level": "INFO",
|
||||
"plane.api": {
|
||||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane": {
|
||||
"plane.worker": {
|
||||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.exception": {
|
||||
"level": "DEBUG" if DEBUG else "ERROR",
|
||||
"handlers": ["console", "file"],
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,4 +41,4 @@ class WorkSpaceCreateReadUpdateDelete(AuthenticatedAPITest):
|
||||
response = self.client.post(
|
||||
url, {"name": "Plane", "slug": "pla-ne"}, format="json"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_410_GONE)
|
||||
self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
|
||||
|
||||
@@ -8,8 +8,8 @@ from django.conf import settings
|
||||
|
||||
def log_exception(e):
|
||||
# Log the error
|
||||
logger = logging.getLogger("plane")
|
||||
logger.error(e)
|
||||
logger = logging.getLogger("plane.exception")
|
||||
logger.error(str(e))
|
||||
|
||||
if settings.DEBUG:
|
||||
# Print the traceback if in debug mode
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
import hashlib
|
||||
|
||||
|
||||
def is_valid_uuid(uuid_str):
|
||||
"""Check if a string is a valid UUID version 4"""
|
||||
try:
|
||||
uuid_obj = uuid.UUID(uuid_str)
|
||||
return uuid_obj.version == 4
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def convert_uuid_to_integer(uuid_val: uuid.UUID) -> int:
|
||||
"""Convert a UUID to a 64-bit signed integer"""
|
||||
# Ensure UUID is a string
|
||||
uuid_value: str = str(uuid_val)
|
||||
# Hash to 64-bit signed int
|
||||
h: bytes = hashlib.sha256(uuid_value.encode()).digest()
|
||||
bigint: int = int.from_bytes(h[:8], byteorder="big", signed=True)
|
||||
return bigint
|
||||
@@ -1,8 +0,0 @@
|
||||
import uuid
|
||||
|
||||
def is_valid_uuid(uuid_str):
|
||||
try:
|
||||
uuid.UUID(uuid_str, version=4)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
@@ -43,7 +43,7 @@ scout-apm==3.1.0
|
||||
# xlsx generation
|
||||
openpyxl==3.1.2
|
||||
# logging
|
||||
python-json-logger==2.0.7
|
||||
python-json-logger==3.3.0
|
||||
# html parser
|
||||
beautifulsoup4==4.12.3
|
||||
# analytics
|
||||
|
||||
@@ -8,3 +8,18 @@ export const ISSUE_REACTION_EMOJI_CODES = [
|
||||
"9992",
|
||||
"128064",
|
||||
];
|
||||
|
||||
export const RANDOM_EMOJI_CODES = [
|
||||
"8986",
|
||||
"9200",
|
||||
"128204",
|
||||
"127773",
|
||||
"127891",
|
||||
"128076",
|
||||
"128077",
|
||||
"128187",
|
||||
"128188",
|
||||
"128512",
|
||||
"128522",
|
||||
"128578",
|
||||
];
|
||||
|
||||
@@ -1,91 +1,97 @@
|
||||
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
|
||||
|
||||
export enum EInboxIssueCurrentTab {
|
||||
OPEN = "open",
|
||||
CLOSED = "closed",
|
||||
OPEN = "open",
|
||||
CLOSED = "closed",
|
||||
}
|
||||
|
||||
export enum EInboxIssueStatus {
|
||||
PENDING = -2,
|
||||
DECLINED = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
PENDING = -2,
|
||||
DECLINED = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
}
|
||||
|
||||
export enum EInboxIssueSource {
|
||||
IN_APP = "IN_APP",
|
||||
FORMS = "FORMS",
|
||||
EMAIL = "EMAIL",
|
||||
}
|
||||
|
||||
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
|
||||
export type TInboxIssueStatus = EInboxIssueStatus;
|
||||
export type TInboxIssue = {
|
||||
id: string;
|
||||
status: TInboxIssueStatus;
|
||||
snoozed_till: Date | null;
|
||||
duplicate_to: string | undefined;
|
||||
source: string;
|
||||
issue: TIssue;
|
||||
created_by: string;
|
||||
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
|
||||
id: string;
|
||||
status: TInboxIssueStatus;
|
||||
snoozed_till: Date | null;
|
||||
duplicate_to: string | undefined;
|
||||
source: EInboxIssueSource | undefined;
|
||||
issue: TIssue;
|
||||
created_by: string;
|
||||
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
|
||||
};
|
||||
|
||||
export const INBOX_STATUS: {
|
||||
key: string;
|
||||
status: TInboxIssueStatus;
|
||||
i18n_title: string;
|
||||
i18n_description: () => string;
|
||||
key: string;
|
||||
status: TInboxIssueStatus;
|
||||
i18n_title: string;
|
||||
i18n_description: () => string;
|
||||
}[] = [
|
||||
{
|
||||
key: "pending",
|
||||
i18n_title: "inbox_issue.status.pending.title",
|
||||
status: EInboxIssueStatus.PENDING,
|
||||
i18n_description: () => `inbox_issue.status.pending.description`,
|
||||
},
|
||||
{
|
||||
key: "declined",
|
||||
i18n_title: "inbox_issue.status.declined.title",
|
||||
status: EInboxIssueStatus.DECLINED,
|
||||
i18n_description: () => `inbox_issue.status.declined.description`,
|
||||
},
|
||||
{
|
||||
key: "snoozed",
|
||||
i18n_title: "inbox_issue.status.snoozed.title",
|
||||
status: EInboxIssueStatus.SNOOZED,
|
||||
i18n_description: () => `inbox_issue.status.snoozed.description`,
|
||||
},
|
||||
{
|
||||
key: "accepted",
|
||||
i18n_title: "inbox_issue.status.accepted.title",
|
||||
status: EInboxIssueStatus.ACCEPTED,
|
||||
i18n_description: () => `inbox_issue.status.accepted.description`,
|
||||
},
|
||||
{
|
||||
key: "duplicate",
|
||||
i18n_title: "inbox_issue.status.duplicate.title",
|
||||
status: EInboxIssueStatus.DUPLICATE,
|
||||
i18n_description: () => `inbox_issue.status.duplicate.description`,
|
||||
},
|
||||
{
|
||||
key: "pending",
|
||||
i18n_title: "inbox_issue.status.pending.title",
|
||||
status: EInboxIssueStatus.PENDING,
|
||||
i18n_description: () => `inbox_issue.status.pending.description`,
|
||||
},
|
||||
{
|
||||
key: "declined",
|
||||
i18n_title: "inbox_issue.status.declined.title",
|
||||
status: EInboxIssueStatus.DECLINED,
|
||||
i18n_description: () => `inbox_issue.status.declined.description`,
|
||||
},
|
||||
{
|
||||
key: "snoozed",
|
||||
i18n_title: "inbox_issue.status.snoozed.title",
|
||||
status: EInboxIssueStatus.SNOOZED,
|
||||
i18n_description: () => `inbox_issue.status.snoozed.description`,
|
||||
},
|
||||
{
|
||||
key: "accepted",
|
||||
i18n_title: "inbox_issue.status.accepted.title",
|
||||
status: EInboxIssueStatus.ACCEPTED,
|
||||
i18n_description: () => `inbox_issue.status.accepted.description`,
|
||||
},
|
||||
{
|
||||
key: "duplicate",
|
||||
i18n_title: "inbox_issue.status.duplicate.title",
|
||||
status: EInboxIssueStatus.DUPLICATE,
|
||||
i18n_description: () => `inbox_issue.status.duplicate.description`,
|
||||
},
|
||||
];
|
||||
|
||||
export const INBOX_ISSUE_ORDER_BY_OPTIONS = [
|
||||
{
|
||||
key: "issue__created_at",
|
||||
i18n_label: "inbox_issue.order_by.created_at",
|
||||
},
|
||||
{
|
||||
key: "issue__updated_at",
|
||||
i18n_label: "inbox_issue.order_by.updated_at",
|
||||
},
|
||||
{
|
||||
key: "issue__sequence_id",
|
||||
i18n_label: "inbox_issue.order_by.id",
|
||||
},
|
||||
{
|
||||
key: "issue__created_at",
|
||||
i18n_label: "inbox_issue.order_by.created_at",
|
||||
},
|
||||
{
|
||||
key: "issue__updated_at",
|
||||
i18n_label: "inbox_issue.order_by.updated_at",
|
||||
},
|
||||
{
|
||||
key: "issue__sequence_id",
|
||||
i18n_label: "inbox_issue.order_by.id",
|
||||
},
|
||||
];
|
||||
|
||||
export const INBOX_ISSUE_SORT_BY_OPTIONS = [
|
||||
{
|
||||
key: "asc",
|
||||
i18n_label: "common.sort.asc",
|
||||
},
|
||||
{
|
||||
key: "desc",
|
||||
i18n_label: "common.sort.desc",
|
||||
},
|
||||
{
|
||||
key: "asc",
|
||||
i18n_label: "common.sort.asc",
|
||||
},
|
||||
{
|
||||
key: "desc",
|
||||
i18n_label: "common.sort.desc",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from "./state";
|
||||
export * from "./swr";
|
||||
export * from "./tab-indices";
|
||||
export * from "./user";
|
||||
export * from "./payment";
|
||||
export * from "./workspace";
|
||||
export * from "./stickies";
|
||||
export * from "./cycle";
|
||||
@@ -29,3 +30,5 @@ export * from "./event-tracker";
|
||||
export * from "./spreadsheet";
|
||||
export * from "./dashboard";
|
||||
export * from "./page";
|
||||
export * from "./emoji";
|
||||
export * from "./subscription";
|
||||
|
||||
@@ -41,6 +41,7 @@ export enum EIssueGroupBYServerToProperty {
|
||||
export enum EIssueServiceType {
|
||||
ISSUES = "issues",
|
||||
EPICS = "epics",
|
||||
WORK_ITEMS = "work-items",
|
||||
}
|
||||
|
||||
export enum EIssuesStoreType {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { IPaymentProduct, TBillingFrequency, TProductBillingFrequency } from "@plane/types";
|
||||
|
||||
/**
|
||||
* Enum representing different product subscription types
|
||||
*/
|
||||
export enum EProductSubscriptionEnum {
|
||||
FREE = "FREE",
|
||||
ONE = "ONE",
|
||||
PRO = "PRO",
|
||||
BUSINESS = "BUSINESS",
|
||||
ENTERPRISE = "ENTERPRISE",
|
||||
}
|
||||
|
||||
/**
|
||||
* Default billing frequency for each product subscription type
|
||||
*/
|
||||
export const DEFAULT_PRODUCT_BILLING_FREQUENCY: TProductBillingFrequency = {
|
||||
[EProductSubscriptionEnum.FREE]: undefined,
|
||||
[EProductSubscriptionEnum.ONE]: undefined,
|
||||
[EProductSubscriptionEnum.PRO]: "month",
|
||||
[EProductSubscriptionEnum.BUSINESS]: "month",
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: "month",
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscription types that support billing frequency toggle (monthly/yearly)
|
||||
*/
|
||||
export const SUBSCRIPTION_WITH_BILLING_FREQUENCY = [
|
||||
EProductSubscriptionEnum.PRO,
|
||||
EProductSubscriptionEnum.BUSINESS,
|
||||
EProductSubscriptionEnum.ENTERPRISE,
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapping of product subscription types to their respective payment product details
|
||||
* Used to provide information about each product's pricing and features
|
||||
*/
|
||||
export const PLANE_COMMUNITY_PRODUCTS: Record<string, IPaymentProduct> = {
|
||||
[EProductSubscriptionEnum.PRO]: {
|
||||
id: EProductSubscriptionEnum.PRO,
|
||||
name: "Plane Pro",
|
||||
description:
|
||||
"More views, more cycles powers, more pages features, new reports, and better dashboards are waiting to be unlocked.",
|
||||
type: "PRO",
|
||||
prices: [
|
||||
{
|
||||
id: `price_monthly_${EProductSubscriptionEnum.PRO}`,
|
||||
unit_amount: 800,
|
||||
recurring: "month",
|
||||
currency: "usd",
|
||||
workspace_amount: 800,
|
||||
product: EProductSubscriptionEnum.PRO,
|
||||
},
|
||||
{
|
||||
id: `price_yearly_${EProductSubscriptionEnum.PRO}`,
|
||||
unit_amount: 7200,
|
||||
recurring: "year",
|
||||
currency: "usd",
|
||||
workspace_amount: 7200,
|
||||
product: EProductSubscriptionEnum.PRO,
|
||||
},
|
||||
],
|
||||
payment_quantity: 1,
|
||||
is_active: true,
|
||||
},
|
||||
[EProductSubscriptionEnum.BUSINESS]: {
|
||||
id: EProductSubscriptionEnum.BUSINESS,
|
||||
name: "Plane Business",
|
||||
description:
|
||||
"The earliest packaging of Business at $10 a seat a month billed annually, $12 a seat a month billed monthly for Plane Cloud",
|
||||
type: "BUSINESS",
|
||||
prices: [
|
||||
{
|
||||
id: `price_yearly_${EProductSubscriptionEnum.BUSINESS}`,
|
||||
unit_amount: 0,
|
||||
recurring: "year",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
product: EProductSubscriptionEnum.BUSINESS,
|
||||
},
|
||||
{
|
||||
id: `price_monthly_${EProductSubscriptionEnum.BUSINESS}`,
|
||||
unit_amount: 0,
|
||||
recurring: "month",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
product: EProductSubscriptionEnum.BUSINESS,
|
||||
},
|
||||
],
|
||||
payment_quantity: 1,
|
||||
is_active: false,
|
||||
},
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: {
|
||||
id: EProductSubscriptionEnum.ENTERPRISE,
|
||||
name: "Plane Enterprise",
|
||||
description: "",
|
||||
type: "ENTERPRISE",
|
||||
prices: [
|
||||
{
|
||||
id: `price_yearly_${EProductSubscriptionEnum.ENTERPRISE}`,
|
||||
unit_amount: 0,
|
||||
recurring: "year",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
product: EProductSubscriptionEnum.ENTERPRISE,
|
||||
},
|
||||
{
|
||||
id: `price_monthly_${EProductSubscriptionEnum.ENTERPRISE}`,
|
||||
unit_amount: 0,
|
||||
recurring: "month",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
product: EProductSubscriptionEnum.ENTERPRISE,
|
||||
},
|
||||
],
|
||||
payment_quantity: 1,
|
||||
is_active: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* URL for the "Talk to Sales" page where users can contact sales team
|
||||
*/
|
||||
export const TALK_TO_SALES_URL = "https://plane.so/talk-to-sales";
|
||||
|
||||
/**
|
||||
* Mapping of subscription types to their respective upgrade/redirection URLs based on billing frequency
|
||||
* Used for self-hosted installations to redirect users to appropriate upgrade pages
|
||||
*/
|
||||
export const SUBSCRIPTION_REDIRECTION_URLS: Record<EProductSubscriptionEnum, Record<TBillingFrequency, string>> = {
|
||||
[EProductSubscriptionEnum.FREE]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
},
|
||||
[EProductSubscriptionEnum.ONE]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
},
|
||||
[EProductSubscriptionEnum.PRO]: {
|
||||
month: "https://app.plane.so/upgrade/pro/self-hosted?plan=month",
|
||||
year: "https://app.plane.so/upgrade/pro/self-hosted?plan=year",
|
||||
},
|
||||
[EProductSubscriptionEnum.BUSINESS]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
},
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping of subscription types to their respective marketing webpage URLs
|
||||
* Used to direct users to learn more about each plan's features and pricing
|
||||
*/
|
||||
export const SUBSCRIPTION_WEBPAGE_URLS: Record<EProductSubscriptionEnum, string> = {
|
||||
[EProductSubscriptionEnum.FREE]: TALK_TO_SALES_URL,
|
||||
[EProductSubscriptionEnum.ONE]: TALK_TO_SALES_URL,
|
||||
[EProductSubscriptionEnum.PRO]: "https://plane.so/pro",
|
||||
[EProductSubscriptionEnum.BUSINESS]: "https://plane.so/business",
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: "https://plane.so/business",
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
// icons
|
||||
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
|
||||
// plane imports
|
||||
import { IProject, TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
|
||||
// local imports
|
||||
import { RANDOM_EMOJI_CODES } from "./emoji";
|
||||
|
||||
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
|
||||
|
||||
@@ -132,3 +134,18 @@ export const PROJECT_ERROR_MESSAGES = {
|
||||
i18n_message: "workspace_projects.error.issue_delete",
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_PROJECT_FORM_VALUES: Partial<IProject> = {
|
||||
cover_image_url: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)],
|
||||
description: "",
|
||||
logo_props: {
|
||||
in_use: "emoji",
|
||||
emoji: {
|
||||
value: RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)],
|
||||
},
|
||||
},
|
||||
identifier: "",
|
||||
name: "",
|
||||
network: 2,
|
||||
project_lead: null,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
export type TStateGroups =
|
||||
| "backlog"
|
||||
| "unstarted"
|
||||
| "started"
|
||||
| "completed"
|
||||
| "cancelled";
|
||||
export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||
|
||||
export type TDraggableData = {
|
||||
groupKey: TStateGroups;
|
||||
@@ -14,40 +9,43 @@ export const STATE_GROUPS: {
|
||||
[key in TStateGroups]: {
|
||||
key: TStateGroups;
|
||||
label: string;
|
||||
defaultStateName: string;
|
||||
color: string;
|
||||
};
|
||||
} = {
|
||||
backlog: {
|
||||
key: "backlog",
|
||||
label: "Backlog",
|
||||
defaultStateName: "Backlog",
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
unstarted: {
|
||||
key: "unstarted",
|
||||
label: "Unstarted",
|
||||
defaultStateName: "Todo",
|
||||
color: "#3f76ff",
|
||||
},
|
||||
started: {
|
||||
key: "started",
|
||||
label: "Started",
|
||||
defaultStateName: "In Progress",
|
||||
color: "#f59e0b",
|
||||
},
|
||||
completed: {
|
||||
key: "completed",
|
||||
label: "Completed",
|
||||
defaultStateName: "Done",
|
||||
color: "#16a34a",
|
||||
},
|
||||
cancelled: {
|
||||
key: "cancelled",
|
||||
label: "Canceled",
|
||||
defaultStateName: "Cancelled",
|
||||
color: "#dc2626",
|
||||
},
|
||||
};
|
||||
|
||||
export const ARCHIVABLE_STATE_GROUPS = [
|
||||
STATE_GROUPS.completed.key,
|
||||
STATE_GROUPS.cancelled.key,
|
||||
];
|
||||
export const ARCHIVABLE_STATE_GROUPS = [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key];
|
||||
export const COMPLETED_STATE_GROUPS = [STATE_GROUPS.completed.key];
|
||||
export const PENDING_STATE_GROUPS = [
|
||||
STATE_GROUPS.backlog.key,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
export const ENTERPRISE_PLAN_FEATURES = [
|
||||
"Private + managed deployments",
|
||||
"GAC",
|
||||
"LDAP support",
|
||||
"Databases + Formulas",
|
||||
"Unlimited and full Automation Flows",
|
||||
"Full-suite professional services",
|
||||
];
|
||||
|
||||
export const BUSINESS_PLAN_FEATURES = [
|
||||
"Project Templates",
|
||||
"Workflows + Approvals",
|
||||
"Decision + Loops Automation",
|
||||
"Custom Reports",
|
||||
"Nested Pages",
|
||||
"Intake Forms",
|
||||
];
|
||||
|
||||
export const PRO_PLAN_FEATURES = [
|
||||
"Dashboards + Reports",
|
||||
"Full Time Tracking + Bulk Ops",
|
||||
"Teamspaces",
|
||||
"Trigger And Action",
|
||||
"Wikis",
|
||||
"Popular integrations",
|
||||
];
|
||||
|
||||
export const ONE_PLAN_FEATURES = [
|
||||
"OIDC + SAML for SSO",
|
||||
"Active Cycles",
|
||||
"Real-time collab + public views and page",
|
||||
"Link pages in issues and vice-versa",
|
||||
"Time-tracking + limited bulk ops",
|
||||
"Docker, Kubernetes and more",
|
||||
];
|
||||
|
||||
export const FREE_PLAN_UPGRADE_FEATURES = [
|
||||
"OIDC + SAML for SSO",
|
||||
"Time Tracking and Bulk Ops",
|
||||
"Integrations",
|
||||
"Public Views and Pages",
|
||||
];
|
||||
@@ -47,15 +47,23 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert a new paragraph at the end of the document
|
||||
const endPosition = editor?.state.doc.content.size;
|
||||
editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run();
|
||||
// Get the last node in the document
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const lastNodePos = editor.state.doc.resolve(Math.max(0, docSize - 2));
|
||||
const lastNode = lastNodePos.node();
|
||||
|
||||
// Focus the newly added paragraph for immediate editing
|
||||
editor
|
||||
.chain()
|
||||
.setTextSelection(endPosition + 1)
|
||||
.run();
|
||||
// Check if the last node is a not paragraph
|
||||
if (lastNode && lastNode.type.name !== "paragraph") {
|
||||
// If last node is not a paragraph, insert a new paragraph at the end
|
||||
const endPosition = editor?.state.doc.content.size;
|
||||
editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run();
|
||||
|
||||
// Focus the newly added paragraph for immediate editing
|
||||
editor
|
||||
.chain()
|
||||
.setTextSelection(endPosition + 1)
|
||||
.run();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("An error occurred while handling container click to insert new empty node at bottom:", error);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custo
|
||||
import { TReadOnlyFileHandler } from "@/types";
|
||||
|
||||
export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
|
||||
const { getAssetSrc } = props;
|
||||
const { getAssetSrc, restore: restoreImageFn } = props;
|
||||
|
||||
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
|
||||
name: "imageComponent",
|
||||
@@ -66,6 +66,9 @@ export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
|
||||
addCommands() {
|
||||
return {
|
||||
getImageSource: (path: string) => async () => await getAssetSrc(path),
|
||||
restoreImage: (src) => async () => {
|
||||
await restoreImageFn(src);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -145,8 +145,8 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
clearEditor: (emitUpdate = false) => {
|
||||
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
setEditorValue: (content: string, emitUpdate = false) => {
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" });
|
||||
},
|
||||
setEditorValueAtCursorPosition: (content: string) => {
|
||||
if (editor?.state.selection) {
|
||||
|
||||
@@ -77,8 +77,8 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
clearEditor: (emitUpdate = false) => {
|
||||
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
setEditorValue: (content: string, emitUpdate = false) => {
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" });
|
||||
},
|
||||
getMarkDown: (): string => {
|
||||
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
||||
|
||||
@@ -84,7 +84,7 @@ export type EditorReadOnlyRefApi = {
|
||||
json: JSONContent | null;
|
||||
};
|
||||
clearEditor: (emitUpdate?: boolean) => void;
|
||||
setEditorValue: (content: string) => void;
|
||||
setEditorValue: (content: string, emitUpdate?: boolean) => void;
|
||||
scrollSummary: (marking: IMarking) => void;
|
||||
getDocumentInfo: () => {
|
||||
characters: number;
|
||||
|
||||
@@ -151,11 +151,11 @@
|
||||
/* end font size and style */
|
||||
|
||||
/* layout config */
|
||||
#page-header-container {
|
||||
container-name: page-header-container;
|
||||
#page-toolbar-container {
|
||||
container-name: page-toolbar-container;
|
||||
container-type: inline-size;
|
||||
|
||||
.page-header-content {
|
||||
.page-toolbar-content {
|
||||
--header-width: var(--normal-content-width);
|
||||
|
||||
&.wide-layout {
|
||||
@@ -186,23 +186,23 @@
|
||||
}
|
||||
|
||||
/* keep a static padding of 96px for wide layouts for container width >912px and <1344px */
|
||||
@container page-header-container (min-width: 912px) and (max-width: 1344px) {
|
||||
.page-header-content.wide-layout {
|
||||
@container page-toolbar-container (min-width: 912px) and (max-width: 1344px) {
|
||||
.page-toolbar-content.wide-layout {
|
||||
padding-left: var(--wide-content-margin) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* keep a static padding of 96px for wide layouts for container width <912px */
|
||||
@container page-header-container (max-width: 912) {
|
||||
.page-header-content.wide-layout {
|
||||
@container page-toolbar-container (max-width: 912) {
|
||||
.page-toolbar-content.wide-layout {
|
||||
padding-left: var(--wide-content-margin) !important;
|
||||
}
|
||||
}
|
||||
/* end layout config */
|
||||
|
||||
/* keep a static padding of 20px for wide layouts for container width <760px */
|
||||
@container page-header-container (max-width: 760px) {
|
||||
.page-header-content {
|
||||
@container page-toolbar-container (max-width: 760px) {
|
||||
.page-toolbar-content {
|
||||
padding-left: var(--normal-content-margin) !important;
|
||||
}
|
||||
}
|
||||
@@ -211,7 +211,7 @@
|
||||
/* keep a static padding of 96px for wide layouts for container width >912px and <1344px */
|
||||
@container page-content-container (min-width: 912px) and (max-width: 1344px) {
|
||||
.editor-container.wide-layout,
|
||||
.page-title-container {
|
||||
.page-header-container {
|
||||
padding-left: var(--wide-content-margin);
|
||||
padding-right: var(--wide-content-margin);
|
||||
}
|
||||
@@ -220,7 +220,7 @@
|
||||
/* keep a static padding of 20px for wide layouts for container width <912px */
|
||||
@container page-content-container (max-width: 912px) {
|
||||
.editor-container.wide-layout,
|
||||
.page-title-container {
|
||||
.page-header-container {
|
||||
padding-left: var(--normal-content-margin);
|
||||
padding-right: var(--normal-content-margin);
|
||||
}
|
||||
@@ -229,7 +229,7 @@
|
||||
/* keep a static padding of 20px for normal layouts for container width <760px */
|
||||
@container page-content-container (max-width: 760px) {
|
||||
.editor-container:not(.wide-layout),
|
||||
.page-title-container {
|
||||
.page-header-container {
|
||||
padding-left: var(--normal-content-margin);
|
||||
padding-right: var(--normal-content-margin);
|
||||
}
|
||||
|
||||
@@ -590,10 +590,10 @@
|
||||
"default": "Zatím nemáte žádné nedávné položky."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Všechny položky",
|
||||
"all": "Vše",
|
||||
"projects": "Projekty",
|
||||
"pages": "Stránky",
|
||||
"issues": "Pracovní položky"
|
||||
"issues": "Úkoly"
|
||||
}
|
||||
},
|
||||
"new_at_plane": {
|
||||
@@ -867,7 +867,8 @@
|
||||
"deleting": "Mazání",
|
||||
"pending": "Čekající",
|
||||
"invite": "Pozvat",
|
||||
"view": "Pohled"
|
||||
"view": "Pohled",
|
||||
"deactivated_user": "Deaktivovaný uživatel"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1734,32 +1735,111 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Povolit odhady v projektu",
|
||||
"description": "Pomáhají komunikovat složitost a vytížení týmu."
|
||||
"label": "Odhady",
|
||||
"title": "Povolit odhady pro můj projekt",
|
||||
"description": "Pomáhají vám komunikovat složitost a pracovní zátěž týmu.",
|
||||
"no_estimate": "Bez odhadu",
|
||||
"new": "Nový systém odhadů",
|
||||
"create": {
|
||||
"custom": "Vlastní",
|
||||
"start_from_scratch": "Začít od nuly",
|
||||
"choose_template": "Vybrat šablonu",
|
||||
"choose_estimate_system": "Vybrat systém odhadů",
|
||||
"enter_estimate_point": "Zadat odhad",
|
||||
"step": "Krok {step} z {total}",
|
||||
"label": "Vytvořit odhad"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Odhad vytvořen",
|
||||
"message": "Odhad byl úspěšně vytvořen"
|
||||
},
|
||||
"error": {
|
||||
"title": "Vytvoření odhadu selhalo",
|
||||
"message": "Nepodařilo se vytvořit nový odhad, zkuste to prosím znovu."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Odhad upraven",
|
||||
"message": "Odhad byl aktualizován ve vašem projektu."
|
||||
},
|
||||
"error": {
|
||||
"title": "Úprava odhadu selhala",
|
||||
"message": "Nepodařilo se upravit odhad, zkuste to prosím znovu"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Úspěch!",
|
||||
"message": "Odhady byly povoleny."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Úspěch!",
|
||||
"message": "Odhady byly zakázány."
|
||||
},
|
||||
"error": {
|
||||
"title": "Chyba!",
|
||||
"message": "Odhad nemohl být zakázán. Zkuste to prosím znovu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Odhad musí být větší než 0.",
|
||||
"unable_to_process": "Nemůžeme zpracovat váš požadavek, zkuste to prosím znovu.",
|
||||
"numeric": "Odhad musí být číselná hodnota.",
|
||||
"character": "Odhad musí být znakový.",
|
||||
"empty": "Hodnota odhadu nemůže být prázdná.",
|
||||
"already_exists": "Hodnota odhadu již existuje.",
|
||||
"unsaved_changes": "Máte neuložené změny. Před kliknutím na hotovo je prosím uložte",
|
||||
"remove_empty": "Odhad nemůže být prázdný. Zadejte hodnotu do každého pole nebo odstraňte ta, pro která nemáte hodnoty."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Body",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Lineární",
|
||||
"squares": "Čtverce",
|
||||
"custom": "Vlastní"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Kategorie",
|
||||
"t_shirt_sizes": "Velikosti triček",
|
||||
"easy_to_hard": "Od snadného po těžké",
|
||||
"custom": "Vlastní"
|
||||
},
|
||||
"time": {
|
||||
"label": "Čas",
|
||||
"hours": "Hodiny"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automatizace",
|
||||
"auto-archive": {
|
||||
"title": "Automaticky archivovat uzavřené položky",
|
||||
"description": "Plane bude archivovat dokončené nebo zrušené položky.",
|
||||
"duration": "Archivovat položky uzavřené déle než"
|
||||
"title": "Automaticky archivovat uzavřené pracovní položky",
|
||||
"description": "Plane bude automaticky archivovat pracovní položky, které byly dokončeny nebo zrušeny.",
|
||||
"duration": "Automaticky archivovat pracovní položky, které jsou uzavřené po dobu"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "Automaticky uzavírat položky",
|
||||
"description": "Plane uzavře neaktivní položky.",
|
||||
"duration": "Uzavřít položky neaktivní déle než",
|
||||
"auto_close_status": "Stav pro automatické uzavření"
|
||||
"title": "Automaticky uzavírat pracovní položky",
|
||||
"description": "Plane automaticky uzavře pracovní položky, které nebyly dokončeny nebo zrušeny.",
|
||||
"duration": "Automaticky uzavřít pracovní položky, které jsou neaktivní po dobu",
|
||||
"auto_close_status": "Stav automatického uzavření"
|
||||
}
|
||||
},
|
||||
|
||||
"empty_state": {
|
||||
"labels": {
|
||||
"title": "Žádné štítky",
|
||||
"description": "Vytvářejte štítky pro organizaci položek."
|
||||
"title": "Zatím žádné štítky",
|
||||
"description": "Vytvořte štítky pro organizaci a filtrování pracovních položek ve vašem projektu."
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Žádné systémy odhadů",
|
||||
"description": "Vytvořte systém odhadů pro komunikaci vytížení.",
|
||||
"title": "Zatím žádné systémy odhadů",
|
||||
"description": "Vytvořte sadu odhadů pro komunikaci množství práce na pracovní položku.",
|
||||
"primary_button": "Přidat systém odhadů"
|
||||
}
|
||||
}
|
||||
@@ -2372,5 +2452,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Modul} few {Moduly} other {Modulů}}",
|
||||
"no_module": "Žádný modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Naposledy upraveno uživatelem",
|
||||
"previously_edited_by": "Dříve upraveno uživatelem",
|
||||
"edited_by": "Upraveno uživatelem"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,7 +500,7 @@
|
||||
"export": "Exportieren",
|
||||
"member": "{count, plural, one{# Mitglied} few{# Mitglieder} other{# Mitglieder}}",
|
||||
"new_password_must_be_different_from_old_password": "Das neue Passwort muss von dem alten Passwort abweichen",
|
||||
|
||||
|
||||
"project_view": {
|
||||
"sort_by": {
|
||||
"created_at": "Erstellt am",
|
||||
@@ -587,7 +587,7 @@
|
||||
"default": "Sie haben noch keine kürzlichen Elemente."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Alle Elemente",
|
||||
"all": "Alle",
|
||||
"projects": "Projekte",
|
||||
"pages": "Seiten",
|
||||
"issues": "Arbeitselemente"
|
||||
@@ -862,7 +862,8 @@
|
||||
"deleting": "Wird gelöscht",
|
||||
"pending": "Ausstehend",
|
||||
"invite": "Einladen",
|
||||
"view": "Ansicht"
|
||||
"view": "Ansicht",
|
||||
"deactivated_user": "Deaktivierter Benutzer"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X-Achse",
|
||||
@@ -1710,21 +1711,100 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Schätzungen im Projekt aktivieren",
|
||||
"description": "Hilft, die Komplexität und Auslastung für das Team zu kommunizieren."
|
||||
"label": "Schätzungen",
|
||||
"title": "Schätzungen für mein Projekt aktivieren",
|
||||
"description": "Sie helfen dir, die Komplexität und Arbeitsbelastung des Teams zu kommunizieren.",
|
||||
"no_estimate": "Keine Schätzung",
|
||||
"new": "Neues Schätzungssystem",
|
||||
"create": {
|
||||
"custom": "Benutzerdefiniert",
|
||||
"start_from_scratch": "Von Grund auf neu",
|
||||
"choose_template": "Vorlage wählen",
|
||||
"choose_estimate_system": "Schätzungssystem wählen",
|
||||
"enter_estimate_point": "Schätzung eingeben",
|
||||
"step": "Schritt {step} von {total}",
|
||||
"label": "Schätzung erstellen"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Schätzung erstellt",
|
||||
"message": "Die Schätzung wurde erfolgreich erstellt"
|
||||
},
|
||||
"error": {
|
||||
"title": "Schätzungserstellung fehlgeschlagen",
|
||||
"message": "Wir konnten die neue Schätzung nicht erstellen, bitte versuche es erneut."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Schätzung geändert",
|
||||
"message": "Die Schätzung wurde in deinem Projekt aktualisiert."
|
||||
},
|
||||
"error": {
|
||||
"title": "Schätzungsänderung fehlgeschlagen",
|
||||
"message": "Wir konnten die Schätzung nicht ändern, bitte versuche es erneut"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Erfolg!",
|
||||
"message": "Schätzungen wurden aktiviert."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Erfolg!",
|
||||
"message": "Schätzungen wurden deaktiviert."
|
||||
},
|
||||
"error": {
|
||||
"title": "Fehler!",
|
||||
"message": "Schätzung konnte nicht deaktiviert werden. Bitte versuche es erneut"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Die Schätzung muss größer als 0 sein.",
|
||||
"unable_to_process": "Wir können deine Anfrage nicht verarbeiten, bitte versuche es erneut.",
|
||||
"numeric": "Die Schätzung muss ein numerischer Wert sein.",
|
||||
"character": "Die Schätzung muss ein Zeichenwert sein.",
|
||||
"empty": "Der Schätzungswert darf nicht leer sein.",
|
||||
"already_exists": "Der Schätzungswert existiert bereits.",
|
||||
"unsaved_changes": "Du hast ungespeicherte Änderungen. Bitte speichere sie, bevor du auf Fertig klickst",
|
||||
"remove_empty": "Die Schätzung darf nicht leer sein. Gib einen Wert in jedes Feld ein oder entferne die Felder, für die du keine Werte hast."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Punkte",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Linear",
|
||||
"squares": "Quadrate",
|
||||
"custom": "Benutzerdefiniert"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Kategorien",
|
||||
"t_shirt_sizes": "T-Shirt-Größen",
|
||||
"easy_to_hard": "Einfach bis schwer",
|
||||
"custom": "Benutzerdefiniert"
|
||||
},
|
||||
"time": {
|
||||
"label": "Zeit",
|
||||
"hours": "Stunden"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automatisierung",
|
||||
"label": "Automatisierungen",
|
||||
"auto-archive": {
|
||||
"title": "Abgeschlossene Elemente automatisch archivieren",
|
||||
"description": "Plane archiviert abgeschlossene oder abgebrochene Elemente.",
|
||||
"duration": "Archivieren von Elementen, die seit mehr als folgendem Zeitraum abgeschlossen sind"
|
||||
"title": "Geschlossene Arbeitselemente automatisch archivieren",
|
||||
"description": "Plane wird Arbeitselemente automatisch archivieren, die abgeschlossen oder abgebrochen wurden.",
|
||||
"duration": "Arbeitselemente automatisch archivieren, die seit"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "Elemente automatisch schließen",
|
||||
"description": "Plane schließt inaktive Elemente.",
|
||||
"duration": "Schließen von Elementen, die seit mehr als folgendem Zeitraum inaktiv sind",
|
||||
"auto_close_status": "Status für automatisches Schließen"
|
||||
"title": "Arbeitselemente automatisch schließen",
|
||||
"description": "Plane wird Arbeitselemente automatisch schließen, die nicht abgeschlossen oder abgebrochen wurden.",
|
||||
"duration": "Inaktive Arbeitselemente automatisch schließen seit",
|
||||
"auto_close_status": "Status der automatischen Schließung"
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
@@ -2321,12 +2401,20 @@
|
||||
"manual": "Manuell"
|
||||
}
|
||||
},
|
||||
|
||||
"cycle": {
|
||||
"label": "{count, plural, one {Zyklus} few {Zyklen} other {Zyklen}}",
|
||||
"no_cycle": "Kein Zyklus"
|
||||
},
|
||||
|
||||
"module": {
|
||||
"label": "{count, plural, one {Modul} few {Module} other {Module}}",
|
||||
"no_module": "Kein Modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Zuletzt bearbeitet von",
|
||||
"previously_edited_by": "Zuvor bearbeitet von",
|
||||
"edited_by": "Bearbeitet von"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@
|
||||
"default": "You don't have any recents yet."
|
||||
},
|
||||
"filters": {
|
||||
"all": "All items",
|
||||
"all": "All",
|
||||
"projects": "Projects",
|
||||
"pages": "Pages",
|
||||
"issues": "Work items"
|
||||
@@ -630,7 +630,8 @@
|
||||
"clear_sorting": "Clear sorting",
|
||||
"show_weekends": "Show weekends",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable"
|
||||
"disable": "Disable",
|
||||
"copy_markdown": "Copy markdown"
|
||||
},
|
||||
"name": "Name",
|
||||
"discard": "Discard",
|
||||
@@ -701,7 +702,8 @@
|
||||
"deleting": "Deleting",
|
||||
"pending": "Pending",
|
||||
"invite": "Invite",
|
||||
"view": "View"
|
||||
"view": "View",
|
||||
"deactivated_user": "Deactivated user"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1568,8 +1570,87 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"label": "Estimates",
|
||||
"title": "Enable estimates for my project",
|
||||
"description": "They help you in communicating complexity and workload of the team."
|
||||
"description": "They help you in communicating complexity and workload of the team.",
|
||||
"no_estimate": "No estimate",
|
||||
"new": "New estimate system",
|
||||
"create": {
|
||||
"custom": "Custom",
|
||||
"start_from_scratch": "Start from scratch",
|
||||
"choose_template": "Choose a template",
|
||||
"choose_estimate_system": "Choose an estimate system",
|
||||
"enter_estimate_point": "Enter estimate",
|
||||
"step": "Step {step} of {total}",
|
||||
"label": "Create estimate"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Estimate created",
|
||||
"message": "The estimate has been created successfully"
|
||||
},
|
||||
"error": {
|
||||
"title": "Estimate creation failed",
|
||||
"message": "We were unable to create the new estimate, please try again."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Estimate modified",
|
||||
"message": "The estimate has been updated in your project."
|
||||
},
|
||||
"error": {
|
||||
"title": "Estimate modification failed",
|
||||
"message": "We were unable to modify the estimate, please try again"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Success!",
|
||||
"message": "Estimates have been enabled."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Success!",
|
||||
"message": "Estimates have been disabled."
|
||||
},
|
||||
"error": {
|
||||
"title": "Error!",
|
||||
"message": "Estimate could not be disabled. Please try again"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Estimate needs to be greater than 0.",
|
||||
"unable_to_process": "We are unable to process your request, please try again.",
|
||||
"numeric": "Estimate needs to be a numeric value.",
|
||||
"character": "Estimate needs to be a character value.",
|
||||
"empty": "Estimate value cannot be empty.",
|
||||
"already_exists": "Estimate value already exists.",
|
||||
"unsaved_changes": "You have some unsaved changes, Please save them before clicking on done",
|
||||
"remove_empty": "Estimate can't be empty. Enter a value in each field or remove those you don't have values for."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Points",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Linear",
|
||||
"squares": "Squares",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Categories",
|
||||
"t_shirt_sizes": "T-Shirt Sizes",
|
||||
"easy_to_hard": "Easy to hard",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"time": {
|
||||
"label": "Time",
|
||||
"hours": "Hours"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automations",
|
||||
@@ -2206,5 +2287,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Module} other {Modules}}",
|
||||
"no_module": "No module"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Last edited by",
|
||||
"previously_edited_by": "Previously edited by",
|
||||
"edited_by": "Edited by"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +596,7 @@
|
||||
"default": "Aún no tienes elementos recientes."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Todos los elementos",
|
||||
"all": "Todos",
|
||||
"projects": "Proyectos",
|
||||
"pages": "Páginas",
|
||||
"issues": "Elementos de trabajo"
|
||||
@@ -872,7 +872,8 @@
|
||||
"deleting": "Eliminando",
|
||||
"pending": "Pendiente",
|
||||
"invite": "Invitar",
|
||||
"view": "Ver"
|
||||
"view": "Ver",
|
||||
"deactivated_user": "Usuario desactivado"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1738,32 +1739,111 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Habilitar estimaciones para mi proyecto",
|
||||
"description": "Ayudan a comunicar la complejidad y la carga de trabajo del equipo."
|
||||
"label": "Estimaciones",
|
||||
"title": "Activar estimaciones para mi proyecto",
|
||||
"description": "Te ayudan a comunicar la complejidad y la carga de trabajo del equipo.",
|
||||
"no_estimate": "Sin estimación",
|
||||
"new": "Nuevo sistema de estimación",
|
||||
"create": {
|
||||
"custom": "Personalizado",
|
||||
"start_from_scratch": "Comenzar desde cero",
|
||||
"choose_template": "Elegir una plantilla",
|
||||
"choose_estimate_system": "Elegir un sistema de estimación",
|
||||
"enter_estimate_point": "Ingresar estimación",
|
||||
"step": "Paso {step} de {total}",
|
||||
"label": "Crear estimación"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Estimación creada",
|
||||
"message": "La estimación se ha creado correctamente"
|
||||
},
|
||||
"error": {
|
||||
"title": "Error al crear la estimación",
|
||||
"message": "No pudimos crear la nueva estimación, por favor inténtalo de nuevo."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Estimación modificada",
|
||||
"message": "La estimación se ha actualizado en tu proyecto."
|
||||
},
|
||||
"error": {
|
||||
"title": "Error al modificar la estimación",
|
||||
"message": "No pudimos modificar la estimación, por favor inténtalo de nuevo"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "¡Éxito!",
|
||||
"message": "Las estimaciones han sido activadas."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "¡Éxito!",
|
||||
"message": "Las estimaciones han sido desactivadas."
|
||||
},
|
||||
"error": {
|
||||
"title": "¡Error!",
|
||||
"message": "No se pudo desactivar la estimación. Por favor inténtalo de nuevo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "La estimación debe ser mayor que 0.",
|
||||
"unable_to_process": "No podemos procesar tu solicitud, por favor inténtalo de nuevo.",
|
||||
"numeric": "La estimación debe ser un valor numérico.",
|
||||
"character": "La estimación debe ser un valor de carácter.",
|
||||
"empty": "El valor de la estimación no puede estar vacío.",
|
||||
"already_exists": "El valor de la estimación ya existe.",
|
||||
"unsaved_changes": "Tienes cambios sin guardar. Por favor guárdalos antes de hacer clic en Hecho",
|
||||
"remove_empty": "La estimación no puede estar vacía. Ingresa un valor en cada campo o elimina aquellos para los que no tienes valores."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Puntos",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Lineal",
|
||||
"squares": "Cuadrados",
|
||||
"custom": "Personalizado"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Categorías",
|
||||
"t_shirt_sizes": "Tallas de camiseta",
|
||||
"easy_to_hard": "Fácil a difícil",
|
||||
"custom": "Personalizado"
|
||||
},
|
||||
"time": {
|
||||
"label": "Tiempo",
|
||||
"hours": "Horas"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automatizaciones",
|
||||
"auto-archive": {
|
||||
"title": "Auto-archivar elementos de trabajo cerrados",
|
||||
"description": "Plane archivará automáticamente los elementos de trabajo que se hayan completado o cancelado.",
|
||||
"duration": "Auto-archivar elementos de trabajo que están cerrados por"
|
||||
"title": "Archivar automáticamente elementos de trabajo cerrados",
|
||||
"description": "Plane archivará automáticamente los elementos de trabajo que hayan sido completados o cancelados.",
|
||||
"duration": "Archivar automáticamente elementos de trabajo cerrados durante"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "Auto-cerrar elementos de trabajo",
|
||||
"description": "Plane cerrará automáticamente los elementos de trabajo que no se hayan completado o cancelado.",
|
||||
"duration": "Auto-cerrar elementos de trabajo que están inactivos por",
|
||||
"auto_close_status": "Estado de auto-cierre"
|
||||
"title": "Cerrar automáticamente elementos de trabajo",
|
||||
"description": "Plane cerrará automáticamente los elementos de trabajo que no hayan sido completados o cancelados.",
|
||||
"duration": "Cerrar automáticamente elementos de trabajo inactivos durante",
|
||||
"auto_close_status": "Estado de cierre automático"
|
||||
}
|
||||
},
|
||||
|
||||
"empty_state": {
|
||||
"labels": {
|
||||
"title": "Aún no hay etiquetas",
|
||||
"description": "Crea etiquetas para ayudar a organizar y filtrar elementos de trabajo en tu proyecto."
|
||||
"description": "Crea etiquetas para organizar y filtrar elementos de trabajo en tu proyecto."
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Aún no hay sistemas de estimación",
|
||||
"description": "Crea un conjunto de estimaciones para comunicar la cantidad de trabajo por elemento de trabajo.",
|
||||
"description": "Crea un conjunto de estimaciones para comunicar el volumen de trabajo por elemento de trabajo.",
|
||||
"primary_button": "Agregar sistema de estimación"
|
||||
}
|
||||
}
|
||||
@@ -2376,5 +2456,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Módulo} other {Módulos}}",
|
||||
"no_module": "Sin módulo"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Última edición por",
|
||||
"previously_edited_by": "Editado anteriormente por",
|
||||
"edited_by": "Editado por"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@
|
||||
"default": "Vous n'avez pas encore d'éléments récents."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Tous les éléments",
|
||||
"all": "Tous",
|
||||
"projects": "Projets",
|
||||
"pages": "Pages",
|
||||
"issues": "Éléments de travail"
|
||||
@@ -870,7 +870,8 @@
|
||||
"deleting": "Suppression",
|
||||
"pending": "En attente",
|
||||
"invite": "Inviter",
|
||||
"view": "Afficher"
|
||||
"view": "Afficher",
|
||||
"deactivated_user": "Utilisateur désactivé"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1736,19 +1737,98 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"label": "Estimations",
|
||||
"title": "Activer les estimations pour mon projet",
|
||||
"description": "Elles vous aident à communiquer la complexité et la charge de travail de l'équipe."
|
||||
"description": "Elles vous aident à communiquer la complexité et la charge de travail de l'équipe.",
|
||||
"no_estimate": "Sans estimation",
|
||||
"new": "Nouveau système d'estimation",
|
||||
"create": {
|
||||
"custom": "Personnalisé",
|
||||
"start_from_scratch": "Commencer depuis zéro",
|
||||
"choose_template": "Choisir un modèle",
|
||||
"choose_estimate_system": "Choisir un système d'estimation",
|
||||
"enter_estimate_point": "Saisir une estimation",
|
||||
"step": "Étape {step} de {total}",
|
||||
"label": "Créer une estimation"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Estimation créée",
|
||||
"message": "L'estimation a été créée avec succès"
|
||||
},
|
||||
"error": {
|
||||
"title": "Échec de la création de l'estimation",
|
||||
"message": "Nous n'avons pas pu créer la nouvelle estimation, veuillez réessayer."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Estimation modifiée",
|
||||
"message": "L'estimation a été mise à jour dans votre projet."
|
||||
},
|
||||
"error": {
|
||||
"title": "Échec de la modification de l'estimation",
|
||||
"message": "Nous n'avons pas pu modifier l'estimation, veuillez réessayer"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Succès !",
|
||||
"message": "Les estimations ont été activées."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Succès !",
|
||||
"message": "Les estimations ont été désactivées."
|
||||
},
|
||||
"error": {
|
||||
"title": "Erreur !",
|
||||
"message": "L'estimation n'a pas pu être désactivée. Veuillez réessayer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "L'estimation doit être supérieure à 0.",
|
||||
"unable_to_process": "Nous ne pouvons pas traiter votre demande, veuillez réessayer.",
|
||||
"numeric": "L'estimation doit être une valeur numérique.",
|
||||
"character": "L'estimation doit être une valeur de caractère.",
|
||||
"empty": "La valeur de l'estimation ne peut pas être vide.",
|
||||
"already_exists": "La valeur de l'estimation existe déjà.",
|
||||
"unsaved_changes": "Vous avez des modifications non enregistrées. Veuillez les enregistrer avant de cliquer sur Terminé",
|
||||
"remove_empty": "L'estimation ne peut pas être vide. Saisissez une valeur dans chaque champ ou supprimez ceux pour lesquels vous n'avez pas de valeurs."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Points",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Linéaire",
|
||||
"squares": "Carrés",
|
||||
"custom": "Personnalisé"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Catégories",
|
||||
"t_shirt_sizes": "Tailles de T-Shirt",
|
||||
"easy_to_hard": "Facile à difficile",
|
||||
"custom": "Personnalisé"
|
||||
},
|
||||
"time": {
|
||||
"label": "Temps",
|
||||
"hours": "Heures"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automatisations",
|
||||
"auto-archive": {
|
||||
"title": "Archiver automatiquement les éléments de travail fermés",
|
||||
"description": "Plane archivera automatiquement les éléments de travail qui ont été terminés ou annulés.",
|
||||
"duration": "Archiver automatiquement les éléments de travail qui sont fermés depuis"
|
||||
"description": "Plane archivera automatiquement les éléments de travail qui ont été complétés ou annulés.",
|
||||
"duration": "Archiver automatiquement les éléments de travail fermés depuis"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "Fermer automatiquement les éléments de travail",
|
||||
"description": "Plane fermera automatiquement les éléments de travail qui n'ont pas été terminés ou annulés.",
|
||||
"description": "Plane fermera automatiquement les éléments de travail qui n'ont pas été complétés ou annulés.",
|
||||
"duration": "Fermer automatiquement les éléments de travail inactifs depuis",
|
||||
"auto_close_status": "Statut de fermeture automatique"
|
||||
}
|
||||
@@ -1756,12 +1836,12 @@
|
||||
|
||||
"empty_state": {
|
||||
"labels": {
|
||||
"title": "Aucune étiquette pour le moment",
|
||||
"description": "Créez des étiquettes pour aider à organiser et filtrer les éléments de travail dans votre projet."
|
||||
"title": "Pas encore d'étiquettes",
|
||||
"description": "Créez des étiquettes pour organiser et filtrer les éléments de travail dans votre projet."
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Aucun système d'estimation pour le moment",
|
||||
"description": "Créez un ensemble d'estimations pour communiquer la quantité de travail par élément de travail.",
|
||||
"title": "Pas encore de systèmes d'estimation",
|
||||
"description": "Créez un ensemble d'estimations pour communiquer le volume de travail par élément de travail.",
|
||||
"primary_button": "Ajouter un système d'estimation"
|
||||
}
|
||||
}
|
||||
@@ -2374,5 +2454,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Module} other {Modules}}",
|
||||
"no_module": "Pas de module"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Dernière modification par",
|
||||
"previously_edited_by": "Précédemment modifié par",
|
||||
"edited_by": "Modifié par"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"default": "Anda belum memiliki yang terbaru."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Semua item",
|
||||
"all": "Semua",
|
||||
"projects": "Proyek",
|
||||
"pages": "Halaman",
|
||||
"issues": "Item kerja"
|
||||
@@ -869,7 +869,8 @@
|
||||
"deleting": "Menghapus",
|
||||
"pending": "Tertunda",
|
||||
"invite": "Undang",
|
||||
"view": "Lihat"
|
||||
"view": "Lihat",
|
||||
"deactivated_user": "Pengguna dinonaktifkan"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1736,33 +1737,112 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Aktifkan estimasi untuk proyek saya",
|
||||
"description": "Mereka membantu Anda dalam mengkomunikasikan kompleksitas dan beban kerja tim."
|
||||
"label": "Perkiraan",
|
||||
"title": "Aktifkan perkiraan untuk proyek saya",
|
||||
"description": "Ini membantu Anda dalam mengkomunikasikan kompleksitas dan beban kerja tim.",
|
||||
"no_estimate": "Tidak ada perkiraan",
|
||||
"new": "Sistem perkiraan baru",
|
||||
"create": {
|
||||
"custom": "Kustom",
|
||||
"start_from_scratch": "Mulai dari awal",
|
||||
"choose_template": "Pilih template",
|
||||
"choose_estimate_system": "Pilih sistem perkiraan",
|
||||
"enter_estimate_point": "Masukkan perkiraan",
|
||||
"step": "Langkah {step} dari {total}",
|
||||
"label": "Buat perkiraan"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Perkiraan dibuat",
|
||||
"message": "Perkiraan telah berhasil dibuat"
|
||||
},
|
||||
"error": {
|
||||
"title": "Pembuatan perkiraan gagal",
|
||||
"message": "Kami tidak dapat membuat perkiraan baru, silakan coba lagi."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Perkiraan dimodifikasi",
|
||||
"message": "Perkiraan telah diperbarui dalam proyek Anda."
|
||||
},
|
||||
"error": {
|
||||
"title": "Modifikasi perkiraan gagal",
|
||||
"message": "Kami tidak dapat memodifikasi perkiraan, silakan coba lagi"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Berhasil!",
|
||||
"message": "Perkiraan telah diaktifkan."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Berhasil!",
|
||||
"message": "Perkiraan telah dinonaktifkan."
|
||||
},
|
||||
"error": {
|
||||
"title": "Kesalahan!",
|
||||
"message": "Perkiraan tidak dapat dinonaktifkan. Silakan coba lagi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Perkiraan harus lebih besar dari 0.",
|
||||
"unable_to_process": "Kami tidak dapat memproses permintaan Anda, silakan coba lagi.",
|
||||
"numeric": "Perkiraan harus berupa nilai numerik.",
|
||||
"character": "Perkiraan harus berupa nilai karakter.",
|
||||
"empty": "Nilai perkiraan tidak boleh kosong.",
|
||||
"already_exists": "Nilai perkiraan sudah ada.",
|
||||
"unsaved_changes": "Anda memiliki beberapa perubahan yang belum disimpan, Harap simpan sebelum mengklik selesai",
|
||||
"remove_empty": "Perkiraan tidak boleh kosong. Masukkan nilai di setiap bidang atau hapus yang tidak memiliki nilai."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Poin",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Linear",
|
||||
"squares": "Kuadrat",
|
||||
"custom": "Kustom"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Kategori",
|
||||
"t_shirt_sizes": "Ukuran Baju",
|
||||
"easy_to_hard": "Mudah ke sulit",
|
||||
"custom": "Kustom"
|
||||
},
|
||||
"time": {
|
||||
"label": "Waktu",
|
||||
"hours": "Jam"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Otomatisasi",
|
||||
"auto-archive": {
|
||||
"title": "Otomatis mengarsipkan item kerja yang ditutup",
|
||||
"description": "Plane akan otomatis mengarsipkan item kerja yang telah diselesaikan atau dibatalkan.",
|
||||
"duration": "Otomatis mengarsipkan item kerja yang ditutup untuk"
|
||||
"title": "Arsip otomatis item kerja yang ditutup",
|
||||
"description": "Plane akan mengarsipkan secara otomatis item kerja yang telah selesai atau dibatalkan.",
|
||||
"duration": "Arsip otomatis item kerja yang ditutup selama"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "Otomatis menutup item kerja",
|
||||
"description": "Plane akan secara otomatis menutup item kerja yang belum diselesaikan atau dibatalkan.",
|
||||
"duration": "Otomatis menutup item kerja yang tidak aktif selama",
|
||||
"auto_close_status": "Status otomatis tutup"
|
||||
"title": "Tutup otomatis item kerja",
|
||||
"description": "Plane akan menutup secara otomatis item kerja yang belum selesai atau dibatalkan.",
|
||||
"duration": "Tutup otomatis item kerja yang tidak aktif selama",
|
||||
"auto_close_status": "Status penutupan otomatis"
|
||||
}
|
||||
},
|
||||
|
||||
"empty_state": {
|
||||
"labels": {
|
||||
"title": "Belum ada label",
|
||||
"description": "Buat label untuk membantu mengorganisir dan memfilter item kerja dalam proyek Anda."
|
||||
"description": "Buat label untuk membantu mengatur dan memfilter item kerja dalam proyek Anda."
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Belum ada sistem estimasi",
|
||||
"description": "Buat seperangkat estimasi untuk mengkomunikasikan jumlah pekerjaan per item kerja.",
|
||||
"primary_button": "Tambah sistem estimasi"
|
||||
"title": "Belum ada sistem perkiraan",
|
||||
"description": "Buat serangkaian perkiraan untuk mengkomunikasikan jumlah pekerjaan per item kerja.",
|
||||
"primary_button": "Tambah sistem perkiraan"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2368,5 +2448,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Modul} other {Modul}}",
|
||||
"no_module": "Tidak ada modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Terakhir disunting oleh",
|
||||
"previously_edited_by": "Sebelumnya disunting oleh",
|
||||
"edited_by": "Disunting oleh"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +501,7 @@
|
||||
"export": "Esporta",
|
||||
"member": "{count, plural, one {# membro} other {# membri}}",
|
||||
"new_password_must_be_different_from_old_password": "La nuova password deve essere diversa dalla password precedente",
|
||||
|
||||
|
||||
"edited": "Modificato",
|
||||
"bot": "Bot",
|
||||
|
||||
@@ -594,7 +594,7 @@
|
||||
"default": "Non hai ancora elementi recenti."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Tutti gli elementi",
|
||||
"all": "Tutti",
|
||||
"projects": "Progetti",
|
||||
"pages": "Pagine",
|
||||
"issues": "Elementi di lavoro"
|
||||
@@ -868,7 +868,8 @@
|
||||
"deleting": "Eliminazione in corso",
|
||||
"pending": "In sospeso",
|
||||
"invite": "Invita",
|
||||
"view": "Visualizza"
|
||||
"view": "Visualizza",
|
||||
"deactivated_user": "Utente disattivato"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1735,20 +1736,99 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"label": "Stime",
|
||||
"title": "Abilita le stime per il mio progetto",
|
||||
"description": "Aiutano a comunicare la complessità e il carico di lavoro del team."
|
||||
"description": "Ti aiutano a comunicare la complessità e il carico di lavoro del team.",
|
||||
"no_estimate": "Nessuna stima",
|
||||
"new": "Nuovo sistema di stima",
|
||||
"create": {
|
||||
"custom": "Personalizzato",
|
||||
"start_from_scratch": "Inizia da zero",
|
||||
"choose_template": "Scegli un modello",
|
||||
"choose_estimate_system": "Scegli un sistema di stima",
|
||||
"enter_estimate_point": "Inserisci stima",
|
||||
"step": "Passo {step} di {total}",
|
||||
"label": "Crea stima"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Stima creata",
|
||||
"message": "La stima è stata creata con successo"
|
||||
},
|
||||
"error": {
|
||||
"title": "Creazione stima fallita",
|
||||
"message": "Non siamo riusciti a creare la nuova stima, riprova."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Stima modificata",
|
||||
"message": "La stima è stata aggiornata nel tuo progetto."
|
||||
},
|
||||
"error": {
|
||||
"title": "Modifica stima fallita",
|
||||
"message": "Non siamo riusciti a modificare la stima, riprova"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Successo!",
|
||||
"message": "Le stime sono state abilitate."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Successo!",
|
||||
"message": "Le stime sono state disabilitate."
|
||||
},
|
||||
"error": {
|
||||
"title": "Errore!",
|
||||
"message": "Impossibile disabilitare la stima. Riprova"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "La stima deve essere maggiore di 0.",
|
||||
"unable_to_process": "Non possiamo elaborare la tua richiesta, riprova.",
|
||||
"numeric": "La stima deve essere un valore numerico.",
|
||||
"character": "La stima deve essere un valore di carattere.",
|
||||
"empty": "Il valore della stima non può essere vuoto.",
|
||||
"already_exists": "Il valore della stima esiste già.",
|
||||
"unsaved_changes": "Hai delle modifiche non salvate. Salva prima di cliccare su Fatto",
|
||||
"remove_empty": "La stima non può essere vuota. Inserisci un valore in ogni campo o rimuovi quelli per cui non hai valori."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Punti",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Lineare",
|
||||
"squares": "Quadrati",
|
||||
"custom": "Personalizzato"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Categorie",
|
||||
"t_shirt_sizes": "Taglie T-Shirt",
|
||||
"easy_to_hard": "Da facile a difficile",
|
||||
"custom": "Personalizzato"
|
||||
},
|
||||
"time": {
|
||||
"label": "Tempo",
|
||||
"hours": "Ore"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automazioni",
|
||||
"label": "Automatizzazioni",
|
||||
"auto-archive": {
|
||||
"title": "Archivia automaticamente gli elementi di lavoro chiusi",
|
||||
"description": "Plane archivierà automaticamente gli elementi di lavoro che sono stati completati o annullati.",
|
||||
"duration": "Archivia automaticamente gli elementi di lavoro chiusi da"
|
||||
"description": "Plane archiverà automaticamente gli elementi di lavoro che sono stati completati o annullati.",
|
||||
"duration": "Archivia automaticamente gli elementi di lavoro chiusi per"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "Chiudi automaticamente gli elementi di lavoro",
|
||||
"description": "Plane chiuderà automaticamente gli elementi di lavoro che non sono stati completati o annullati.",
|
||||
"duration": "Chiudi automaticamente gli elementi di lavoro inattivi da",
|
||||
"duration": "Chiudi automaticamente gli elementi di lavoro inattivi per",
|
||||
"auto_close_status": "Stato di chiusura automatica"
|
||||
}
|
||||
},
|
||||
@@ -2373,5 +2453,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Modulo} other {Moduli}}",
|
||||
"no_module": "Nessun modulo"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Ultima modifica di",
|
||||
"previously_edited_by": "Precedentemente modificato da",
|
||||
"edited_by": "Modificato da"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@
|
||||
"default": "まだ最近の項目がありません。"
|
||||
},
|
||||
"filters": {
|
||||
"all": "すべての項目",
|
||||
"all": "すべて",
|
||||
"projects": "プロジェクト",
|
||||
"pages": "ページ",
|
||||
"issues": "作業項目"
|
||||
@@ -870,7 +870,8 @@
|
||||
"deleting": "デリーティング",
|
||||
"pending": "保留中",
|
||||
"invite": "招待",
|
||||
"view": "ビュー"
|
||||
"view": "ビュー",
|
||||
"deactivated_user": "無効化されたユーザー"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1736,20 +1737,99 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"label": "見積もり",
|
||||
"title": "プロジェクトの見積もりを有効にする",
|
||||
"description": "チームの複雑さと作業負荷を伝えるのに役立ちます。"
|
||||
"description": "チームの複雑さと作業負荷を伝えるのに役立ちます。",
|
||||
"no_estimate": "見積もりなし",
|
||||
"new": "新しい見積もりシステム",
|
||||
"create": {
|
||||
"custom": "カスタム",
|
||||
"start_from_scratch": "最初から開始",
|
||||
"choose_template": "テンプレートを選択",
|
||||
"choose_estimate_system": "見積もりシステムを選択",
|
||||
"enter_estimate_point": "見積もりを入力",
|
||||
"step": "ステップ {step} の {total}",
|
||||
"label": "見積もりを作成"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "見積もりを作成",
|
||||
"message": "見積もりが正常に作成されました"
|
||||
},
|
||||
"error": {
|
||||
"title": "見積もり作成に失敗",
|
||||
"message": "新しい見積もりを作成できませんでした。もう一度お試しください。"
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "見積もりを更新",
|
||||
"message": "プロジェクトの見積もりが更新されました。"
|
||||
},
|
||||
"error": {
|
||||
"title": "見積もり更新に失敗",
|
||||
"message": "見積もりを更新できませんでした。もう一度お試しください"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "成功!",
|
||||
"message": "見積もりが有効になりました。"
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "成功!",
|
||||
"message": "見積もりが無効になりました。"
|
||||
},
|
||||
"error": {
|
||||
"title": "エラー!",
|
||||
"message": "見積もりを無効にできませんでした。もう一度お試しください"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "見積もりは0より大きい必要があります。",
|
||||
"unable_to_process": "リクエストを処理できません。もう一度お試しください。",
|
||||
"numeric": "見積もりは数値である必要があります。",
|
||||
"character": "見積もりは文字値である必要があります。",
|
||||
"empty": "見積もり値は空にできません。",
|
||||
"already_exists": "見積もり値は既に存在します。",
|
||||
"unsaved_changes": "未保存の変更があります。完了をクリックする前に保存してください",
|
||||
"remove_empty": "見積もりは空にできません。各フィールドに値を入力するか、値がないフィールドを削除してください。"
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "ポイント",
|
||||
"fibonacci": "フィボナッチ",
|
||||
"linear": "リニア",
|
||||
"squares": "二乗",
|
||||
"custom": "カスタム"
|
||||
},
|
||||
"categories": {
|
||||
"label": "カテゴリー",
|
||||
"t_shirt_sizes": "Tシャツサイズ",
|
||||
"easy_to_hard": "簡単から難しい",
|
||||
"custom": "カスタム"
|
||||
},
|
||||
"time": {
|
||||
"label": "時間",
|
||||
"hours": "時間"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "自動化",
|
||||
"auto-archive": {
|
||||
"title": "完了した作業項目を自動アーカイブ",
|
||||
"title": "完了した作業項目を自動的にアーカイブ",
|
||||
"description": "Planeは完了またはキャンセルされた作業項目を自動的にアーカイブします。",
|
||||
"duration": "次の期間完了している作業項目を自動アーカイブ"
|
||||
"duration": "閉じられた作業項目を自動的にアーカイブ"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "作業項目を自動クローズ",
|
||||
"description": "Planeは完了またはキャンセルされていない作業項目を自動的にクローズします。",
|
||||
"duration": "次の期間非アクティブな作業項目を自動クローズ",
|
||||
"title": "作業項目を自動的に閉じる",
|
||||
"description": "Planeは完了またはキャンセルされていない作業項目を自動的に閉じます。",
|
||||
"duration": "非アクティブな作業項目を自動的に閉じる",
|
||||
"auto_close_status": "自動クローズステータス"
|
||||
}
|
||||
},
|
||||
@@ -2374,5 +2454,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {モジュール} other {モジュール}}",
|
||||
"no_module": "モジュールなし"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "最終編集者",
|
||||
"previously_edited_by": "以前の編集者",
|
||||
"edited_by": "編集者"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@
|
||||
"default": "아직 최근 항목이 없습니다."
|
||||
},
|
||||
"filters": {
|
||||
"all": "모든 항목",
|
||||
"all": "모든",
|
||||
"projects": "프로젝트",
|
||||
"pages": "페이지",
|
||||
"issues": "작업 항목"
|
||||
@@ -871,7 +871,8 @@
|
||||
"deleting": "삭제 중",
|
||||
"pending": "보류 중",
|
||||
"invite": "초대",
|
||||
"view": "보기"
|
||||
"view": "보기",
|
||||
"deactivated_user": "비활성화된 사용자"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1738,20 +1739,99 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "프로젝트에 대한 추정 활성화",
|
||||
"description": "팀의 복잡성과 작업량을 전달하는 데 도움이 됩니다."
|
||||
"label": "추정",
|
||||
"title": "프로젝트 추정 활성화",
|
||||
"description": "팀의 복잡성과 작업량을 전달하는 데 도움이 됩니다.",
|
||||
"no_estimate": "추정 없음",
|
||||
"new": "새 추정 시스템",
|
||||
"create": {
|
||||
"custom": "사용자 지정",
|
||||
"start_from_scratch": "처음부터 시작",
|
||||
"choose_template": "템플릿 선택",
|
||||
"choose_estimate_system": "추정 시스템 선택",
|
||||
"enter_estimate_point": "추정 입력",
|
||||
"step": "단계 {step}/{total}",
|
||||
"label": "추정 생성"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "추정 생성됨",
|
||||
"message": "추정이 성공적으로 생성되었습니다"
|
||||
},
|
||||
"error": {
|
||||
"title": "추정 생성 실패",
|
||||
"message": "새 추정을 생성할 수 없습니다. 다시 시도해 주세요."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "추정 수정됨",
|
||||
"message": "프로젝트의 추정이 업데이트되었습니다."
|
||||
},
|
||||
"error": {
|
||||
"title": "추정 수정 실패",
|
||||
"message": "추정을 수정할 수 없습니다. 다시 시도해 주세요"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "성공!",
|
||||
"message": "추정이 활성화되었습니다."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "성공!",
|
||||
"message": "추정이 비활성화되었습니다."
|
||||
},
|
||||
"error": {
|
||||
"title": "오류!",
|
||||
"message": "추정을 비활성화할 수 없습니다. 다시 시도해 주세요"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "추정은 0보다 커야 합니다.",
|
||||
"unable_to_process": "요청을 처리할 수 없습니다. 다시 시도해 주세요.",
|
||||
"numeric": "추정은 숫자 값이어야 합니다.",
|
||||
"character": "추정은 문자 값이어야 합니다.",
|
||||
"empty": "추정 값은 비어있을 수 없습니다.",
|
||||
"already_exists": "추정 값이 이미 존재합니다.",
|
||||
"unsaved_changes": "저장되지 않은 변경 사항이 있습니다. 완료를 클릭하기 전에 저장하세요",
|
||||
"remove_empty": "추정은 비어있을 수 없습니다. 각 필드에 값을 입력하거나 값이 없는 필드를 제거하세요."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "포인트",
|
||||
"fibonacci": "피보나치",
|
||||
"linear": "선형",
|
||||
"squares": "제곱",
|
||||
"custom": "사용자 정의"
|
||||
},
|
||||
"categories": {
|
||||
"label": "카테고리",
|
||||
"t_shirt_sizes": "티셔츠 사이즈",
|
||||
"easy_to_hard": "쉬움에서 어려움",
|
||||
"custom": "사용자 정의"
|
||||
},
|
||||
"time": {
|
||||
"label": "시간",
|
||||
"hours": "시간"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "자동화",
|
||||
"auto-archive": {
|
||||
"title": "닫힌 작업 항목 자동 아카이브",
|
||||
"description": "Plane은 완료되거나 취소된 작업 항목을 자동으로 아카이브합니다.",
|
||||
"duration": "닫힌 작업 항목을 자동 아카이브"
|
||||
"title": "완료된 작업 항목 자동 보관",
|
||||
"description": "Plane은 완료되거나 취소된 작업 항목을 자동으로 보관합니다.",
|
||||
"duration": "다음 기간 동안 닫힌 작업 항목 자동 보관"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "작업 항목 자동 닫기",
|
||||
"description": "Plane은 완료되거나 취소되지 않은 작업 항목을 자동으로 닫습니다.",
|
||||
"duration": "비활성 상태인 작업 항목 자동 닫기",
|
||||
"duration": "다음 기간 동안 비활성 작업 항목 자동 닫기",
|
||||
"auto_close_status": "자동 닫기 상태"
|
||||
}
|
||||
},
|
||||
@@ -2376,5 +2456,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {모듈} other {모듈}}",
|
||||
"no_module": "모듈 없음"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "마지막 편집자",
|
||||
"previously_edited_by": "이전 편집자",
|
||||
"edited_by": "편집자"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,7 +500,7 @@
|
||||
"export": "Eksportuj",
|
||||
"member": "{count, plural, one{# członek} few{# członkowie} other{# członków}}",
|
||||
"new_password_must_be_different_from_old_password": "Nowe hasło musi być innym niż stare hasło",
|
||||
|
||||
|
||||
"edited": "Edytowano",
|
||||
"bot": "Bot",
|
||||
|
||||
@@ -590,7 +590,7 @@
|
||||
"default": "Nie masz jeszcze żadnych ostatnich pozycji."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Wszystkie pozycje",
|
||||
"all": "Wszystkie",
|
||||
"projects": "Projekty",
|
||||
"pages": "Strony",
|
||||
"issues": "Elementy pracy"
|
||||
@@ -865,7 +865,8 @@
|
||||
"deleting": "Usuwanie",
|
||||
"pending": "Oczekujące",
|
||||
"invite": "Zaproś",
|
||||
"view": "Widok"
|
||||
"view": "Widok",
|
||||
"deactivated_user": "Dezaktywowany użytkownik"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Oś X",
|
||||
@@ -1713,21 +1714,100 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Włącz szacowania w projekcie",
|
||||
"description": "Pomaga komunikować złożoność i obciążenie zespołu."
|
||||
"label": "Szacunki",
|
||||
"title": "Włącz szacunki dla mojego projektu",
|
||||
"description": "Pomagają w komunikacji o złożoności i obciążeniu zespołu.",
|
||||
"no_estimate": "Bez szacunku",
|
||||
"new": "Nowy system szacowania",
|
||||
"create": {
|
||||
"custom": "Niestandardowy",
|
||||
"start_from_scratch": "Zacznij od zera",
|
||||
"choose_template": "Wybierz szablon",
|
||||
"choose_estimate_system": "Wybierz system szacowania",
|
||||
"enter_estimate_point": "Wprowadź punkt szacunkowy",
|
||||
"step": "Krok {step} z {total}",
|
||||
"label": "Utwórz szacunek"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Utworzono szacunek",
|
||||
"message": "Szacunek został utworzony pomyślnie"
|
||||
},
|
||||
"error": {
|
||||
"title": "Błąd tworzenia szacunku",
|
||||
"message": "Nie udało się utworzyć nowego szacunku, spróbuj ponownie."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Zaktualizowano szacunek",
|
||||
"message": "Szacunek został zaktualizowany w Twoim projekcie."
|
||||
},
|
||||
"error": {
|
||||
"title": "Błąd aktualizacji szacunku",
|
||||
"message": "Nie udało się zaktualizować szacunku, spróbuj ponownie"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Sukces!",
|
||||
"message": "Szacunki zostały włączone."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Sukces!",
|
||||
"message": "Szacunki zostały wyłączone."
|
||||
},
|
||||
"error": {
|
||||
"title": "Błąd!",
|
||||
"message": "Nie udało się wyłączyć szacunków. Spróbuj ponownie"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Punkt szacunkowy musi być większy niż 0.",
|
||||
"unable_to_process": "Nie możemy przetworzyć Twojego żądania, spróbuj ponownie.",
|
||||
"numeric": "Punkt szacunkowy musi być wartością liczbową.",
|
||||
"character": "Punkt szacunkowy musi być znakiem.",
|
||||
"empty": "Wartość szacunku nie może być pusta.",
|
||||
"already_exists": "Wartość szacunku już istnieje.",
|
||||
"unsaved_changes": "Masz niezapisane zmiany. Zapisz je przed kliknięciem 'gotowe'",
|
||||
"remove_empty": "Szacunek nie może być pusty. Wprowadź wartość w każde pole lub usuń te, dla których nie masz wartości."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Punkty",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Liniowy",
|
||||
"squares": "Kwadraty",
|
||||
"custom": "Własny"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Kategorie",
|
||||
"t_shirt_sizes": "Rozmiary koszulek",
|
||||
"easy_to_hard": "Od łatwego do trudnego",
|
||||
"custom": "Własne"
|
||||
},
|
||||
"time": {
|
||||
"label": "Czas",
|
||||
"hours": "Godziny"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automatyzacje",
|
||||
"label": "Automatyzacja",
|
||||
"auto-archive": {
|
||||
"title": "Automatycznie archiwizuj zamknięte elementy",
|
||||
"description": "Plane będzie archiwizował ukończone lub anulowane elementy.",
|
||||
"title": "Automatyczna archiwizacja zamkniętych elementów",
|
||||
"description": "Plane będzie automatycznie archiwizował elementy, które zostały ukończone lub anulowane.",
|
||||
"duration": "Archiwizuj elementy zamknięte dłużej niż"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "Automatycznie zamykaj elementy",
|
||||
"description": "Plane będzie zamykać nieaktywne elementy.",
|
||||
"title": "Automatyczne zamykanie elementów",
|
||||
"description": "Plane będzie automatycznie zamykał elementy, które nie zostały ukończone lub anulowane.",
|
||||
"duration": "Zamknij elementy nieaktywne dłużej niż",
|
||||
"auto_close_status": "Stan do automatycznego zamknięcia"
|
||||
"auto_close_status": "Status automatycznego zamknięcia"
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
@@ -2324,12 +2404,20 @@
|
||||
"manual": "Ręcznie"
|
||||
}
|
||||
},
|
||||
|
||||
"cycle": {
|
||||
"label": "{count, plural, one {Cykl} few {Cykle} other {Cyklów}}",
|
||||
"no_cycle": "Brak cyklu"
|
||||
},
|
||||
|
||||
"module": {
|
||||
"label": "{count, plural, one {Moduł} few {Moduły} other {Modułów}}",
|
||||
"no_module": "Brak modułu"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Ostatnio edytowane przez",
|
||||
"previously_edited_by": "Wcześniej edytowane przez",
|
||||
"edited_by": "Edytowane przez"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@
|
||||
"default": "Você não tem nenhum item recente ainda."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Todos os itens",
|
||||
"all": "Todos",
|
||||
"projects": "Projetos",
|
||||
"pages": "Páginas",
|
||||
"issues": "Itens de trabalho"
|
||||
@@ -871,7 +871,8 @@
|
||||
"deleting": "Excluindo",
|
||||
"pending": "Pendente",
|
||||
"invite": "Convidar",
|
||||
"view": "Visualizar"
|
||||
"view": "Visualizar",
|
||||
"deactivated_user": "Usuário desativado"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1738,8 +1739,87 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"label": "Estimativas",
|
||||
"title": "Habilitar estimativas para meu projeto",
|
||||
"description": "Elas ajudam você a comunicar a complexidade e a carga de trabalho da equipe."
|
||||
"description": "Elas ajudam você a comunicar a complexidade e a carga de trabalho da equipe.",
|
||||
"no_estimate": "Sem estimativa",
|
||||
"new": "Novo sistema de estimativa",
|
||||
"create": {
|
||||
"custom": "Personalizado",
|
||||
"start_from_scratch": "Começar do zero",
|
||||
"choose_template": "Escolher um modelo",
|
||||
"choose_estimate_system": "Escolher um sistema de estimativa",
|
||||
"enter_estimate_point": "Inserir estimativa",
|
||||
"step": "Passo {step} de {total}",
|
||||
"label": "Criar estimativa"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Estimativa criada",
|
||||
"message": "A estimativa foi criada com sucesso"
|
||||
},
|
||||
"error": {
|
||||
"title": "Falha na criação da estimativa",
|
||||
"message": "Não foi possível criar a nova estimativa, por favor tente novamente."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Estimativa modificada",
|
||||
"message": "A estimativa foi atualizada em seu projeto."
|
||||
},
|
||||
"error": {
|
||||
"title": "Falha na modificação da estimativa",
|
||||
"message": "Não foi possível modificar a estimativa, por favor tente novamente"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Sucesso!",
|
||||
"message": "As estimativas foram habilitadas."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Sucesso!",
|
||||
"message": "As estimativas foram desabilitadas."
|
||||
},
|
||||
"error": {
|
||||
"title": "Erro!",
|
||||
"message": "Não foi possível desabilitar a estimativa. Por favor, tente novamente"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "A estimativa precisa ser maior que 0.",
|
||||
"unable_to_process": "Não foi possível processar sua solicitação, por favor tente novamente.",
|
||||
"numeric": "A estimativa precisa ser um valor numérico.",
|
||||
"character": "A estimativa precisa ser um valor em caracteres.",
|
||||
"empty": "O valor da estimativa não pode estar vazio.",
|
||||
"already_exists": "O valor da estimativa já existe.",
|
||||
"unsaved_changes": "Você tem algumas alterações não salvas. Por favor, salve-as antes de clicar em concluir",
|
||||
"remove_empty": "A estimativa não pode estar vazia. Insira um valor em cada campo ou remova aqueles para os quais você não tem valores."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Pontos",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Linear",
|
||||
"squares": "Quadrados",
|
||||
"custom": "Personalizado"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Categorias",
|
||||
"t_shirt_sizes": "Tamanhos de Camiseta",
|
||||
"easy_to_hard": "Fácil a difícil",
|
||||
"custom": "Personalizado"
|
||||
},
|
||||
"time": {
|
||||
"label": "Tempo",
|
||||
"hours": "Horas"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automações",
|
||||
@@ -2369,5 +2449,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Módulo} other {Módulos}}",
|
||||
"no_module": "Nenhum módulo"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Última edição por",
|
||||
"previously_edited_by": "Anteriormente editado por",
|
||||
"edited_by": "Editado por"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"default": "Nu ai nimic recent încă."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Toate activitățile",
|
||||
"all": "Toate",
|
||||
"projects": "Proiecte",
|
||||
"pages": "Documentație",
|
||||
"issues": "Activități"
|
||||
@@ -869,7 +869,8 @@
|
||||
"deleting": "Se șterge",
|
||||
"pending": "În așteptare",
|
||||
"invite": "Invită",
|
||||
"view": "Vizualizează"
|
||||
"view": "Vizualizează",
|
||||
"deactivated_user": "Utilizator dezactivat"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1736,8 +1737,87 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"label": "Estimări",
|
||||
"title": "Activează estimările pentru proiectul meu",
|
||||
"description": "Acestea te ajută să comunici complexitatea și volumul de muncă al echipei."
|
||||
"description": "Te ajută să comunici complexitatea și volumul de muncă al echipei.",
|
||||
"no_estimate": "Fără estimare",
|
||||
"new": "Noul sistem de estimare",
|
||||
"create": {
|
||||
"custom": "Personalizat",
|
||||
"start_from_scratch": "Începe de la zero",
|
||||
"choose_template": "Alege un șablon",
|
||||
"choose_estimate_system": "Alege un sistem de estimare",
|
||||
"enter_estimate_point": "Introdu estimarea",
|
||||
"step": "Pasul {step} de {total}",
|
||||
"label": "Creează estimare"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Estimare creată",
|
||||
"message": "Estimarea a fost creată cu succes"
|
||||
},
|
||||
"error": {
|
||||
"title": "Crearea estimării a eșuat",
|
||||
"message": "Nu am putut crea noua estimare, te rugăm să încerci din nou."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Estimare modificată",
|
||||
"message": "Estimarea a fost actualizată în proiectul tău."
|
||||
},
|
||||
"error": {
|
||||
"title": "Modificarea estimării a eșuat",
|
||||
"message": "Nu am putut modifica estimarea, te rugăm să încerci din nou"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Succes!",
|
||||
"message": "Estimările au fost activate."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Succes!",
|
||||
"message": "Estimările au fost dezactivate."
|
||||
},
|
||||
"error": {
|
||||
"title": "Eroare!",
|
||||
"message": "Estimarea nu a putut fi dezactivată. Te rugăm să încerci din nou"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Estimarea trebuie să fie mai mare decât 0.",
|
||||
"unable_to_process": "Nu putem procesa cererea ta, te rugăm să încerci din nou.",
|
||||
"numeric": "Estimarea trebuie să fie o valoare numerică.",
|
||||
"character": "Estimarea trebuie să fie o valoare de tip caracter.",
|
||||
"empty": "Valoarea estimării nu poate fi goală.",
|
||||
"already_exists": "Valoarea estimării există deja.",
|
||||
"unsaved_changes": "Ai modificări nesalvate, te rugăm să le salvezi înainte de a finaliza",
|
||||
"remove_empty": "Estimarea nu poate fi goală. Introdu o valoare în fiecare câmp sau elimină câmpurile pentru care nu ai valori."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Puncte",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Linear",
|
||||
"squares": "Pătrate",
|
||||
"custom": "Personalizat"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Categorii",
|
||||
"t_shirt_sizes": "Mărimi tricou",
|
||||
"easy_to_hard": "De la ușor la greu",
|
||||
"custom": "Personalizat"
|
||||
},
|
||||
"time": {
|
||||
"label": "Timp",
|
||||
"hours": "Ore"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automatizări",
|
||||
@@ -2368,5 +2448,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Modul} other {Module}}",
|
||||
"no_module": "Niciun modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Ultima editare de către",
|
||||
"previously_edited_by": "Editat anterior de către",
|
||||
"edited_by": "Editat de"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"default": "Пока нет недавних элементов"
|
||||
},
|
||||
"filters": {
|
||||
"all": "Все элементы",
|
||||
"all": "Все",
|
||||
"projects": "Проекты",
|
||||
"pages": "Страницы",
|
||||
"issues": "Рабочие элементы"
|
||||
@@ -869,7 +869,8 @@
|
||||
"deleting": "Удаление",
|
||||
"pending": "Ожидание",
|
||||
"invite": "Пригласить",
|
||||
"view": "Просмотр"
|
||||
"view": "Просмотр",
|
||||
"deactivated_user": "Деактивированный пользователь"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1736,33 +1737,112 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Включить оценку для проекта",
|
||||
"description": "Помогают оценивать сложность и загрузку команды."
|
||||
"label": "Оценки",
|
||||
"title": "Включить оценки для моего проекта",
|
||||
"description": "Они помогают вам в общении о сложности и рабочей нагрузке команды.",
|
||||
"no_estimate": "Без оценки",
|
||||
"new": "Новая система оценок",
|
||||
"create": {
|
||||
"custom": "Пользовательская",
|
||||
"start_from_scratch": "Начать с нуля",
|
||||
"choose_template": "Выбрать шаблон",
|
||||
"choose_estimate_system": "Выбрать систему оценок",
|
||||
"enter_estimate_point": "Ввести оценку",
|
||||
"step": "Шаг {step} из {total}",
|
||||
"label": "Создать оценку"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Оценка создана",
|
||||
"message": "Оценка успешно создана"
|
||||
},
|
||||
"error": {
|
||||
"title": "Ошибка создания оценки",
|
||||
"message": "Не удалось создать новую оценку, пожалуйста, попробуйте снова."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Оценка изменена",
|
||||
"message": "Оценка обновлена в вашем проекте."
|
||||
},
|
||||
"error": {
|
||||
"title": "Ошибка изменения оценки",
|
||||
"message": "Не удалось изменить оценку, пожалуйста, попробуйте снова"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Успех!",
|
||||
"message": "Оценки включены."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Успех!",
|
||||
"message": "Оценки отключены."
|
||||
},
|
||||
"error": {
|
||||
"title": "Ошибка!",
|
||||
"message": "Не удалось отключить оценки. Пожалуйста, попробуйте снова"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Оценка должна быть больше 0.",
|
||||
"unable_to_process": "Не удалось обработать ваш запрос, пожалуйста, попробуйте снова.",
|
||||
"numeric": "Оценка должна быть числовым значением.",
|
||||
"character": "Оценка должна быть символьным значением.",
|
||||
"empty": "Значение оценки не может быть пустым.",
|
||||
"already_exists": "Значение оценки уже существует.",
|
||||
"unsaved_changes": "У вас есть несохраненные изменения. Пожалуйста, сохраните их перед нажатием на готово",
|
||||
"remove_empty": "Оценка не может быть пустой. Введите значение в каждое поле или удалите те, для которых у вас нет значений."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Баллы",
|
||||
"fibonacci": "Фибоначчи",
|
||||
"linear": "Линейная",
|
||||
"squares": "Квадраты",
|
||||
"custom": "Пользовательская"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Категории",
|
||||
"t_shirt_sizes": "Размеры футболок",
|
||||
"easy_to_hard": "От простого к сложному",
|
||||
"custom": "Пользовательская"
|
||||
},
|
||||
"time": {
|
||||
"label": "Время",
|
||||
"hours": "Часы"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Автоматизация",
|
||||
"auto-archive": {
|
||||
"title": "Автоархивация закрытых рабочих элементов",
|
||||
"description": "Plane будет автоматически архивировать завершённые или отменённые рабочие элементы.",
|
||||
"duration": "Архивировать рабочие элементы закрытые более"
|
||||
"description": "Plane будет автоматически архивировать рабочие элементы, которые были завершены или отменены.",
|
||||
"duration": "Автоархивация рабочих элементов, которые закрыты в течение"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "Автозакрытие рабочих элементов",
|
||||
"description": "Plane будет автоматически закрывать неактивные рабочие элементы.",
|
||||
"duration": "Закрывать рабочие элементы неактивные более",
|
||||
"auto_close_status": "Статус автозакрытия"
|
||||
"title": "Автоматическое закрытие рабочих элементов",
|
||||
"description": "Plane будет автоматически закрывать рабочие элементы, которые не были завершены или отменены.",
|
||||
"duration": "Автоматическое закрытие рабочих элементов, которые неактивны в течение",
|
||||
"auto_close_status": "Статус автоматического закрытия"
|
||||
}
|
||||
},
|
||||
|
||||
"empty_state": {
|
||||
"labels": {
|
||||
"title": "Нет меток",
|
||||
"description": "Создавайте метки для организации и фильтрации рабочих элементов."
|
||||
"description": "Создайте метки для организации и фильтрации рабочих элементов в вашем проекте."
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Нет систем оценки",
|
||||
"description": "Создайте систему оценок для планирования работ.",
|
||||
"primary_button": "Добавить систему"
|
||||
"title": "Нет систем оценок",
|
||||
"description": "Создайте набор оценок для передачи объема работы на каждый рабочий элемент.",
|
||||
"primary_button": "Добавить систему оценок"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2374,5 +2454,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Модуль} other {Модули}}",
|
||||
"no_module": "Нет модуля"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Последнее редактирование",
|
||||
"previously_edited_by": "Ранее отредактировано",
|
||||
"edited_by": "Отредактировано"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"default": "Zatiaľ nemáte žiadne nedávne položky."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Všetky položky",
|
||||
"all": "Všetko",
|
||||
"projects": "Projekty",
|
||||
"pages": "Stránky",
|
||||
"issues": "Pracovné položky"
|
||||
@@ -869,7 +869,8 @@
|
||||
"deleting": "Mazanie",
|
||||
"pending": "Čakajúce",
|
||||
"invite": "Pozvať",
|
||||
"view": "Zobraziť"
|
||||
"view": "Zobraziť",
|
||||
"deactivated_user": "Deaktivovaný používateľ"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1735,8 +1736,87 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Povoliť odhady v projekte",
|
||||
"description": "Pomáhajú komunikovať zložitosť a vyťaženie tímu."
|
||||
"label": "Odhady",
|
||||
"title": "Povoliť odhady pre môj projekt",
|
||||
"description": "Pomáhajú vám komunikovať zložitosť a pracovné zaťaženie tímu.",
|
||||
"no_estimate": "Bez odhadu",
|
||||
"new": "Nový systém odhadov",
|
||||
"create": {
|
||||
"custom": "Vlastné",
|
||||
"start_from_scratch": "Začať od nuly",
|
||||
"choose_template": "Vybrať šablónu",
|
||||
"choose_estimate_system": "Vybrať systém odhadov",
|
||||
"enter_estimate_point": "Zadať bod odhadu",
|
||||
"step": "Krok {step} z {total}",
|
||||
"label": "Vytvoriť odhad"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Bod odhadu vytvorený",
|
||||
"message": "Bod odhadu bol úspešne vytvorený"
|
||||
},
|
||||
"error": {
|
||||
"title": "Vytvorenie bodu odhadu zlyhalo",
|
||||
"message": "Nepodarilo sa vytvoriť nový bod odhadu, skúste to prosím znova."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Odhad upravený",
|
||||
"message": "Bod odhadu bol aktualizovaný vo vašom projekte."
|
||||
},
|
||||
"error": {
|
||||
"title": "Úprava odhadu zlyhala",
|
||||
"message": "Nepodarilo sa upraviť odhad, skúste to prosím znova"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Úspech!",
|
||||
"message": "Odhady boli povolené."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Úspech!",
|
||||
"message": "Odhady boli zakázané."
|
||||
},
|
||||
"error": {
|
||||
"title": "Chyba!",
|
||||
"message": "Odhad sa nepodarilo zakázať. Skúste to prosím znova"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Bod odhadu musí byť väčší ako 0.",
|
||||
"unable_to_process": "Nemôžeme spracovať vašu požiadavku, skúste to prosím znova.",
|
||||
"numeric": "Bod odhadu musí byť číselná hodnota.",
|
||||
"character": "Bod odhadu musí byť znakovou hodnotou.",
|
||||
"empty": "Hodnota odhadu nemôže byť prázdna.",
|
||||
"already_exists": "Hodnota odhadu už existuje.",
|
||||
"unsaved_changes": "Máte neuložené zmeny. Prosím, uložte ich pred kliknutím na hotovo",
|
||||
"remove_empty": "Odhad nemôže byť prázdny. Zadajte hodnotu do každého poľa alebo odstráňte prázdne polia."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Body",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Lineárne",
|
||||
"squares": "Štvorce",
|
||||
"custom": "Vlastné"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Kategórie",
|
||||
"t_shirt_sizes": "Veľkosti tričiek",
|
||||
"easy_to_hard": "Od jednoduchého po náročné",
|
||||
"custom": "Vlastné"
|
||||
},
|
||||
"time": {
|
||||
"label": "Čas",
|
||||
"hours": "Hodiny"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automatizácie",
|
||||
@@ -2373,5 +2453,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {Modul} few {Moduly} other {Modulov}}",
|
||||
"no_module": "Žiadny modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Naposledy upravené používateľom",
|
||||
"previously_edited_by": "Predtým upravené používateľom",
|
||||
"edited_by": "Upravené používateľom"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,7 +502,7 @@
|
||||
"new_password_must_be_different_from_old_password": "Новий пароль повинен бути відмінним від старого пароля",
|
||||
"edited": "Редагувано",
|
||||
"bot": "Бот",
|
||||
|
||||
|
||||
"project_view": {
|
||||
"sort_by": {
|
||||
"created_at": "Створено",
|
||||
@@ -589,7 +589,7 @@
|
||||
"default": "Поки у вас немає нещодавніх елементів."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Усі елементи",
|
||||
"all": "Усі",
|
||||
"projects": "Проєкти",
|
||||
"pages": "Сторінки",
|
||||
"issues": "Робочі одиниці"
|
||||
@@ -864,7 +864,8 @@
|
||||
"deleting": "Видалення",
|
||||
"pending": "Очікує",
|
||||
"invite": "Запросити",
|
||||
"view": "Подання"
|
||||
"view": "Подання",
|
||||
"deactivated_user": "Деактивований користувач"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Вісь X",
|
||||
@@ -1712,8 +1713,87 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "Увімкнути оцінки в проєкті",
|
||||
"description": "Вони допомагають відображати складність і навантаження на команду."
|
||||
"label": "Оцінки",
|
||||
"title": "Увімкнути оцінки для мого проєкту",
|
||||
"description": "Вони допомагають вам повідомляти про складність та навантаження команди.",
|
||||
"no_estimate": "Без оцінки",
|
||||
"new": "Нова система оцінок",
|
||||
"create": {
|
||||
"custom": "Власний",
|
||||
"start_from_scratch": "Почати з нуля",
|
||||
"choose_template": "Вибрати шаблон",
|
||||
"choose_estimate_system": "Вибрати систему оцінок",
|
||||
"enter_estimate_point": "Введіть оцінку",
|
||||
"step": "Крок {step} з {total}",
|
||||
"label": "Створити оцінку"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Оцінку створено",
|
||||
"message": "Оцінку успішно створено"
|
||||
},
|
||||
"error": {
|
||||
"title": "Не вдалося створити оцінку",
|
||||
"message": "Не вдалося створити нову оцінку, спробуйте ще раз."
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Оцінку змінено",
|
||||
"message": "Оцінку оновлено у вашому проєкті."
|
||||
},
|
||||
"error": {
|
||||
"title": "Не вдалося змінити оцінку",
|
||||
"message": "Не вдалося змінити оцінку, спробуйте ще раз"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Успіх!",
|
||||
"message": "Оцінки увімкнено."
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Успіх!",
|
||||
"message": "Оцінки вимкнено."
|
||||
},
|
||||
"error": {
|
||||
"title": "Помилка!",
|
||||
"message": "Не вдалося вимкнути оцінку. Спробуйте ще раз"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Оцінка має бути більшою за 0.",
|
||||
"unable_to_process": "Не вдалося обробити ваш запит, спробуйте ще раз.",
|
||||
"numeric": "Оцінка має бути числовим значенням.",
|
||||
"character": "Оцінка має бути символьним значенням.",
|
||||
"empty": "Значення оцінки не може бути порожнім.",
|
||||
"already_exists": "Таке значення оцінки вже існує.",
|
||||
"unsaved_changes": "У вас є незбережені зміни. Збережіть їх перед тим, як натиснути 'готово'",
|
||||
"remove_empty": "Оцінка не може бути порожньою. Введіть значення в кожне поле або видаліть ті, для яких у вас немає значень."
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Бали",
|
||||
"fibonacci": "Фібоначчі",
|
||||
"linear": "Лінійна",
|
||||
"squares": "Квадрати",
|
||||
"custom": "Власна"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Категорії",
|
||||
"t_shirt_sizes": "Розміри футболок",
|
||||
"easy_to_hard": "Від легкого до складного",
|
||||
"custom": "Власна"
|
||||
},
|
||||
"time": {
|
||||
"label": "Час",
|
||||
"hours": "Години"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Автоматизація",
|
||||
@@ -2323,12 +2403,20 @@
|
||||
"manual": "Вручну"
|
||||
}
|
||||
},
|
||||
|
||||
"cycle": {
|
||||
"label": "{count, plural, one {Цикл} few {Цикли} other {Циклів}}",
|
||||
"no_cycle": "Немає циклу"
|
||||
},
|
||||
|
||||
"module": {
|
||||
"label": "{count, plural, one {Модуль} few {Модулі} other {Модулів}}",
|
||||
"no_module": "Немає модуля"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Останнє редагування",
|
||||
"previously_edited_by": "Раніше відредаговано",
|
||||
"edited_by": "Відредаговано"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +589,7 @@
|
||||
"default": "Bạn chưa có dự án gần đây nào."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Tất cả dự án",
|
||||
"all": "Tất cả",
|
||||
"projects": "Dự án",
|
||||
"pages": "Trang",
|
||||
"issues": "Mục công việc"
|
||||
@@ -863,7 +863,8 @@
|
||||
"deleting": "Đang xóa",
|
||||
"pending": "Đang chờ xử lý",
|
||||
"invite": "Mời",
|
||||
"view": "Xem"
|
||||
"view": "Xem",
|
||||
"deactivated_user": "Người dùng bị vô hiệu hóa"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Trục X",
|
||||
@@ -1711,8 +1712,86 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"label": "Ước tính",
|
||||
"title": "Bật ước tính cho dự án của tôi",
|
||||
"description": "Chúng giúp bạn truyền đạt độ phức tạp và khối lượng công việc của nhóm."
|
||||
"description": "Chúng giúp bạn truyền đạt độ phức tạp và khối lượng công việc của nhóm.",
|
||||
"no_estimate": "Không có ước tính",
|
||||
"new": "Hệ thống ước tính mới",
|
||||
"create": {
|
||||
"custom": "Tùy chỉnh",
|
||||
"start_from_scratch": "Bắt đầu từ đầu",
|
||||
"choose_template": "Chọn mẫu",
|
||||
"choose_estimate_system": "Chọn hệ thống ước tính",
|
||||
"enter_estimate_point": "Nhập điểm ước tính",
|
||||
"step": "Bước {step} của {total}",
|
||||
"label": "Tạo ước tính"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "Đã tạo điểm ước tính",
|
||||
"message": "Điểm ước tính đã được tạo thành công"
|
||||
},
|
||||
"error": {
|
||||
"title": "Không thể tạo điểm ước tính",
|
||||
"message": "Không thể tạo điểm ước tính mới, vui lòng thử lại"
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "Đã cập nhật ước tính",
|
||||
"message": "Điểm ước tính đã được cập nhật trong dự án của bạn"
|
||||
},
|
||||
"error": {
|
||||
"title": "Không thể cập nhật ước tính",
|
||||
"message": "Không thể cập nhật ước tính, vui lòng thử lại"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "Thành công!",
|
||||
"message": "Đã bật ước tính"
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "Thành công!",
|
||||
"message": "Đã tắt ước tính"
|
||||
},
|
||||
"error": {
|
||||
"title": "Lỗi!",
|
||||
"message": "Không thể tắt ước tính. Vui lòng thử lại"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "Điểm ước tính phải lớn hơn 0",
|
||||
"unable_to_process": "Không thể xử lý yêu cầu của bạn, vui lòng thử lại",
|
||||
"numeric": "Điểm ước tính phải là số",
|
||||
"character": "Điểm ước tính phải là ký tự",
|
||||
"empty": "Giá trị ước tính không được để trống",
|
||||
"already_exists": "Giá trị ước tính này đã tồn tại",
|
||||
"unsaved_changes": "Bạn có thay đổi chưa lưu. Vui lòng lưu trước khi nhấn 'xong'"
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "Điểm",
|
||||
"fibonacci": "Fibonacci",
|
||||
"linear": "Tuyến tính",
|
||||
"squares": "Bình phương",
|
||||
"custom": "Tùy chỉnh"
|
||||
},
|
||||
"categories": {
|
||||
"label": "Danh mục",
|
||||
"t_shirt_sizes": "Kích cỡ áo",
|
||||
"easy_to_hard": "Dễ đến khó",
|
||||
"custom": "Tùy chỉnh"
|
||||
},
|
||||
"time": {
|
||||
"label": "Thời gian",
|
||||
"hours": "Giờ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "Tự động hóa",
|
||||
@@ -2322,12 +2401,20 @@
|
||||
"manual": "Thủ công"
|
||||
}
|
||||
},
|
||||
|
||||
"cycle": {
|
||||
"label": "{count, plural, one {chu kỳ} other {chu kỳ}}",
|
||||
"no_cycle": "Không có chu kỳ"
|
||||
},
|
||||
|
||||
"module": {
|
||||
"label": "{count, plural, one {mô-đun} other {mô-đun}}",
|
||||
"no_module": "Không có mô-đun"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Chỉnh sửa lần cuối bởi",
|
||||
"previously_edited_by": "Trước đây được chỉnh sửa bởi",
|
||||
"edited_by": "Được chỉnh sửa bởi"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@
|
||||
"default": "您还没有任何最近项目。"
|
||||
},
|
||||
"filters": {
|
||||
"all": "所有项目",
|
||||
"all": "所有",
|
||||
"projects": "项目",
|
||||
"pages": "页面",
|
||||
"issues": "工作项"
|
||||
@@ -870,7 +870,8 @@
|
||||
"deleting": "删除中",
|
||||
"pending": "待处理",
|
||||
"invite": "邀请",
|
||||
"view": "查看"
|
||||
"view": "查看",
|
||||
"deactivated_user": "已停用用户"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1736,8 +1737,68 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"label": "估算",
|
||||
"title": "为我的项目启用估算",
|
||||
"description": "它们有助于您传达团队的复杂性和工作量。"
|
||||
"description": "它们有助于您传达团队的复杂性和工作量。",
|
||||
"no_estimate": "无估算",
|
||||
"new": "新估算系统",
|
||||
"create": {
|
||||
"custom": "自定义",
|
||||
"start_from_scratch": "从头开始",
|
||||
"choose_template": "选择模板",
|
||||
"choose_estimate_system": "选择估算系统",
|
||||
"enter_estimate_point": "输入估算点数",
|
||||
"step": "步骤 {step} 共 {total}",
|
||||
"label": "创建估算"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "已创建估算点数",
|
||||
"message": "估算点数创建成功"
|
||||
},
|
||||
"error": {
|
||||
"title": "无法创建估算点数",
|
||||
"message": "无法创建新的估算点数,请重试"
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "已更新估算",
|
||||
"message": "您项目中的估算点数已更新"
|
||||
},
|
||||
"error": {
|
||||
"title": "无法更新估算",
|
||||
"message": "无法更新估算,请重试"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "成功!",
|
||||
"message": "已启用估算"
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "成功!",
|
||||
"message": "已禁用估算"
|
||||
},
|
||||
"error": {
|
||||
"title": "错误!",
|
||||
"message": "无法禁用估算。请重试"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "估算需要大于0。",
|
||||
"unable_to_process": "我们无法处理您的请求,请重试。",
|
||||
"numeric": "估算需要是数值。",
|
||||
"character": "估算需要是字符值。",
|
||||
"empty": "估算值不能为空。",
|
||||
"already_exists": "估算值已存在。",
|
||||
"unsaved_changes": "您有未保存的更改,请在点击完成前保存。",
|
||||
"remove_empty": "估算不能为空。请在每个字段中输入值或删除没有值的字段。"
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "自动化",
|
||||
@@ -2374,5 +2435,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {模块} other {模块}}",
|
||||
"no_module": "无模块"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "最后编辑者",
|
||||
"previously_edited_by": "之前编辑者",
|
||||
"edited_by": "编辑者"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@
|
||||
"default": "您還沒有任何最近項目。"
|
||||
},
|
||||
"filters": {
|
||||
"all": "所有項目",
|
||||
"all": "所有",
|
||||
"projects": "專案",
|
||||
"pages": "頁面",
|
||||
"issues": "工作事項"
|
||||
@@ -871,7 +871,8 @@
|
||||
"deleting": "刪除中",
|
||||
"pending": "待處理",
|
||||
"invite": "邀請",
|
||||
"view": "檢視"
|
||||
"view": "檢視",
|
||||
"deactivated_user": "已停用用戶"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -1738,20 +1739,99 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"title": "為我的專案啟用評估",
|
||||
"description": "它們可以協助您傳達團隊的複雜度和工作量。"
|
||||
"label": "預估",
|
||||
"title": "為我的專案啟用預估",
|
||||
"description": "幫助你傳達團隊的複雜性和工作負荷。",
|
||||
"no_estimate": "無預估",
|
||||
"new": "新估算系統",
|
||||
"create": {
|
||||
"custom": "自訂",
|
||||
"start_from_scratch": "從頭開始",
|
||||
"choose_template": "選擇範本",
|
||||
"choose_estimate_system": "選擇預估系統",
|
||||
"enter_estimate_point": "輸入預估",
|
||||
"step": "步驟 {step} 共 {total}",
|
||||
"label": "建立預估"
|
||||
},
|
||||
"toasts": {
|
||||
"created": {
|
||||
"success": {
|
||||
"title": "預估已建立",
|
||||
"message": "預估已成功建立"
|
||||
},
|
||||
"error": {
|
||||
"title": "預估建立失敗",
|
||||
"message": "我們無法建立新的預估,請重試。"
|
||||
}
|
||||
},
|
||||
"updated": {
|
||||
"success": {
|
||||
"title": "預估已修改",
|
||||
"message": "專案中的預估已更新。"
|
||||
},
|
||||
"error": {
|
||||
"title": "預估修改失敗",
|
||||
"message": "我們無法修改預估,請重試"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"success": {
|
||||
"title": "成功!",
|
||||
"message": "預估已啟用。"
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"success": {
|
||||
"title": "成功!",
|
||||
"message": "預估已停用。"
|
||||
},
|
||||
"error": {
|
||||
"title": "錯誤!",
|
||||
"message": "無法停用預估。請重試"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"min_length": "預估必須大於0。",
|
||||
"unable_to_process": "我們無法處理你的請求,請重試。",
|
||||
"numeric": "預估必須是數值。",
|
||||
"character": "預估必須是字元值。",
|
||||
"empty": "預估值不能為空。",
|
||||
"already_exists": "預估值已存在。",
|
||||
"unsaved_changes": "你有未儲存的變更。請在點擊完成前儲存",
|
||||
"remove_empty": "預估不能為空。在每個欄位中輸入值或移除沒有值的欄位。"
|
||||
},
|
||||
"systems": {
|
||||
"points": {
|
||||
"label": "點數",
|
||||
"fibonacci": "費波那契數列",
|
||||
"linear": "線性",
|
||||
"squares": "平方數",
|
||||
"custom": "自訂"
|
||||
},
|
||||
"categories": {
|
||||
"label": "類別",
|
||||
"t_shirt_sizes": "T恤尺寸",
|
||||
"easy_to_hard": "簡單到困難",
|
||||
"custom": "自訂"
|
||||
},
|
||||
"time": {
|
||||
"label": "時間",
|
||||
"hours": "小時"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automations": {
|
||||
"label": "自動化",
|
||||
"auto-archive": {
|
||||
"title": "自動封存已關閉的工作事項",
|
||||
"description": "Plane 將自動封存已完成或取消的工作事項。",
|
||||
"duration": "自動封存已關閉"
|
||||
"title": "自動封存已關閉的工作項目",
|
||||
"description": "Plane將自動封存已完成或已取消的工作項目。",
|
||||
"duration": "自動封存已關閉的工作項目"
|
||||
},
|
||||
"auto-close": {
|
||||
"title": "自動關閉工作事項",
|
||||
"description": "Plane 將自動關閉尚未完成或取消的工作事項。",
|
||||
"duration": "自動關閉閒置",
|
||||
"title": "自動關閉工作項目",
|
||||
"description": "Plane將自動關閉未完成或未取消的工作項目。",
|
||||
"duration": "自動關閉非活動工作項目",
|
||||
"auto_close_status": "自動關閉狀態"
|
||||
}
|
||||
},
|
||||
@@ -2376,5 +2456,11 @@
|
||||
"module": {
|
||||
"label": "{count, plural, one {模組} other {模組}}",
|
||||
"no_module": "無模組"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "最後編輯者",
|
||||
"previously_edited_by": "先前編輯者",
|
||||
"edited_by": "編輯者"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ export const BarChart = React.memo(<K extends string, T extends string>(props: T
|
||||
dataKey={bar.key}
|
||||
stackId={bar.stackId}
|
||||
opacity={!!activeLegend && activeLegend !== bar.key ? 0.1 : 1}
|
||||
fill={bar.fill}
|
||||
shape={(shapeProps: any) => {
|
||||
const showTopBorderRadius = bar.showTopBorderRadius?.(shapeProps.dataKey, shapeProps.payload);
|
||||
const showBottomBorderRadius = bar.showBottomBorderRadius?.(shapeProps.dataKey, shapeProps.payload);
|
||||
@@ -64,6 +63,7 @@ export const BarChart = React.memo(<K extends string, T extends string>(props: T
|
||||
return (
|
||||
<CustomBar
|
||||
{...shapeProps}
|
||||
fill={typeof bar.fill === "function" ? bar.fill(shapeProps.payload) : bar.fill}
|
||||
stackKeys={stackKeys}
|
||||
textClassName={bar.textClassName}
|
||||
showPercentage={bar.showPercentage}
|
||||
|
||||
@@ -27,6 +27,7 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
"custom-shadow": "var(--color-shadow-custom)",
|
||||
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
||||
"custom-shadow-xs": "var(--color-shadow-xs)",
|
||||
"custom-shadow-sm": "var(--color-shadow-sm)",
|
||||
@@ -208,6 +209,28 @@ module.exports = {
|
||||
hover: "rgba(96, 100, 108, 0.25)",
|
||||
active: "rgba(96, 100, 108, 0.7)",
|
||||
},
|
||||
subscription: {
|
||||
free: {
|
||||
200: convertToRGB("--color-subscription-free-200"),
|
||||
400: convertToRGB("--color-subscription-free-400"),
|
||||
},
|
||||
one: {
|
||||
200: convertToRGB("--color-subscription-one-200"),
|
||||
400: convertToRGB("--color-subscription-one-400"),
|
||||
},
|
||||
pro: {
|
||||
200: convertToRGB("--color-subscription-pro-200"),
|
||||
400: convertToRGB("--color-subscription-pro-400"),
|
||||
},
|
||||
business: {
|
||||
200: convertToRGB("--color-subscription-business-200"),
|
||||
400: convertToRGB("--color-subscription-business-400"),
|
||||
},
|
||||
enterprise: {
|
||||
200: convertToRGB("--color-subscription-enterprise-200"),
|
||||
400: convertToRGB("--color-subscription-enterprise-400"),
|
||||
},
|
||||
},
|
||||
},
|
||||
onboarding: {
|
||||
background: {
|
||||
|
||||
Vendored
+2
-1
@@ -43,7 +43,7 @@ type TChartProps<K extends string, T extends string> = {
|
||||
export type TBarItem<T extends string> = {
|
||||
key: T;
|
||||
label: string;
|
||||
fill: string;
|
||||
fill: string | ((payload: any) => string);
|
||||
textClassName: string;
|
||||
showPercentage?: boolean;
|
||||
stackId: string;
|
||||
@@ -116,6 +116,7 @@ export type TPieChartProps<K extends string, T extends string> = Pick<
|
||||
text?: string | number;
|
||||
};
|
||||
tooltipLabel?: string | ((payload: any) => string);
|
||||
customLegend?: (props: any) => React.ReactNode;
|
||||
};
|
||||
|
||||
export type TreeMapItem = {
|
||||
|
||||
Vendored
+8
@@ -26,3 +26,11 @@ export type TLogoProps = {
|
||||
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
|
||||
|
||||
export type TFetchStatus = "partial" | "complete" | undefined;
|
||||
|
||||
export type ICustomSearchSelectOption = {
|
||||
value: any;
|
||||
query: string;
|
||||
content: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | React.ReactNode;
|
||||
};
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
export type TDescriptionVersion = {
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
id: string;
|
||||
last_saved_at: string;
|
||||
owned_by: string;
|
||||
project: string;
|
||||
updated_at: string;
|
||||
updated_by: string | null;
|
||||
};
|
||||
|
||||
export type TDescriptionVersionDetails = TDescriptionVersion & {
|
||||
description_binary: string | null;
|
||||
description_html: string | null;
|
||||
description_json: object | null;
|
||||
description_stripped: string | null;
|
||||
};
|
||||
|
||||
export type TDescriptionVersionsListResponse = {
|
||||
cursor: string;
|
||||
next_cursor: string | null;
|
||||
next_page_results: boolean;
|
||||
page_count: number;
|
||||
prev_cursor: string | null;
|
||||
prev_page_results: boolean;
|
||||
results: TDescriptionVersion[];
|
||||
total_pages: number;
|
||||
total_results: number;
|
||||
};
|
||||
Vendored
+4
-7
@@ -14,10 +14,7 @@ export interface IEstimatePoint {
|
||||
updated_by: string | undefined;
|
||||
}
|
||||
|
||||
export type TEstimateSystemKeys =
|
||||
| EEstimateSystem.POINTS
|
||||
| EEstimateSystem.CATEGORIES
|
||||
| EEstimateSystem.TIME;
|
||||
export type TEstimateSystemKeys = EEstimateSystem.POINTS | EEstimateSystem.CATEGORIES | EEstimateSystem.TIME;
|
||||
|
||||
export interface IEstimate {
|
||||
id: string | undefined;
|
||||
@@ -55,12 +52,14 @@ export type TEstimatePointsObject = {
|
||||
|
||||
export type TTemplateValues = {
|
||||
title: string;
|
||||
i18n_title: string;
|
||||
values: TEstimatePointsObject[];
|
||||
hide?: boolean;
|
||||
};
|
||||
|
||||
export type TEstimateSystem = {
|
||||
name: string;
|
||||
i18n_name: string;
|
||||
templates: Record<string, TTemplateValues>;
|
||||
is_available: boolean;
|
||||
is_ee: boolean;
|
||||
@@ -82,6 +81,4 @@ export type TEstimateTypeErrorObject = {
|
||||
message: string | undefined;
|
||||
};
|
||||
|
||||
export type TEstimateTypeError =
|
||||
| Record<number, TEstimateTypeErrorObject>
|
||||
| undefined;
|
||||
export type TEstimateTypeError = Record<number, TEstimateTypeErrorObject> | undefined;
|
||||
|
||||
Vendored
+3
-1
@@ -1,6 +1,8 @@
|
||||
// plane constants
|
||||
import { TInboxIssue, TInboxIssueStatus } from "@plane/constants";
|
||||
// plane types
|
||||
import { TPaginationInfo } from "./common";
|
||||
import { TIssuePriorities } from "./issues";
|
||||
import { TIssue } from "./issues/base";
|
||||
|
||||
// filters
|
||||
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
|
||||
|
||||
Vendored
+2
@@ -3,6 +3,7 @@ export * from "./workspace";
|
||||
export * from "./cycle";
|
||||
export * from "./dashboard";
|
||||
export * from "./de-dupe";
|
||||
export * from "./description_version";
|
||||
export * from "./project";
|
||||
export * from "./state";
|
||||
export * from "./issues";
|
||||
@@ -41,3 +42,4 @@ export * from "./charts";
|
||||
export * from "./home";
|
||||
export * from "./stickies";
|
||||
export * from "./utils";
|
||||
export * from "./payment";
|
||||
|
||||
+2
-3
@@ -37,6 +37,7 @@ export interface IInstance {
|
||||
}
|
||||
|
||||
export interface IInstanceConfig {
|
||||
enable_signup: boolean;
|
||||
is_workspace_creation_disabled: boolean;
|
||||
is_google_enabled: boolean;
|
||||
is_github_enabled: boolean;
|
||||
@@ -72,9 +73,7 @@ export interface IInstanceAdmin {
|
||||
user_detail: IUserLite;
|
||||
}
|
||||
|
||||
export type TInstanceIntercomConfigurationKeys =
|
||||
| "IS_INTERCOM_ENABLED"
|
||||
| "INTERCOM_APP_ID";
|
||||
export type TInstanceIntercomConfigurationKeys = "IS_INTERCOM_ENABLED" | "INTERCOM_APP_ID";
|
||||
|
||||
export type TInstanceConfigurationKeys =
|
||||
| TInstanceAIConfigurationKeys
|
||||
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
// plane imports
|
||||
import { EInboxIssueSource } from "@plane/constants";
|
||||
// local imports
|
||||
import {
|
||||
TIssueActivityWorkspaceDetail,
|
||||
TIssueActivityProjectDetail,
|
||||
@@ -31,7 +34,7 @@ export type TIssueActivity = {
|
||||
epoch: number;
|
||||
issue_comment: string | null;
|
||||
source_data: {
|
||||
source: "IN_APP" | "FORM" | "EMAIL";
|
||||
source: EInboxIssueSource;
|
||||
source_email?: string;
|
||||
extra: {
|
||||
username?: string;
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ export type TIssueComment = {
|
||||
};
|
||||
|
||||
export type TCommentsOperations = {
|
||||
createComment: (data: Partial<TIssueComment>) => Promise<void>;
|
||||
createComment: (data: Partial<TIssueComment>) => Promise<Partial<TIssueComment> | undefined>;
|
||||
updateComment: (commentId: string, data: Partial<TIssueComment>) => Promise<void>;
|
||||
removeComment: (commentId: string) => Promise<void>;
|
||||
uploadCommentAsset: (blockId: string, file: File, commentId?: string) => Promise<TFileSignedURLResponse>;
|
||||
|
||||
Vendored
+1
-1
@@ -120,7 +120,7 @@ export type TBulkOperationsPayload = {
|
||||
|
||||
export type TIssueDetailWidget = "sub-issues" | "relations" | "links" | "attachments";
|
||||
|
||||
export type TIssueServiceType = EIssueServiceType.ISSUES | EIssueServiceType.EPICS;
|
||||
export type TIssueServiceType = EIssueServiceType.ISSUES | EIssueServiceType.EPICS | EIssueServiceType.WORK_ITEMS;
|
||||
|
||||
export interface IPublicIssue
|
||||
extends Pick<
|
||||
|
||||
+17
@@ -20,3 +20,20 @@ export type TIssueSubIssuesStateDistributionMap = {
|
||||
export type TIssueSubIssuesIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
|
||||
export type TSubIssueOperations = {
|
||||
copyLink: (path: string) => void;
|
||||
fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise<void>;
|
||||
addSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => Promise<void>;
|
||||
updateSubIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue?: Partial<TIssue>,
|
||||
fromModal?: boolean
|
||||
) => Promise<void>;
|
||||
removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import { EProductSubscriptionEnum } from "@plane/constants";
|
||||
|
||||
export type TBillingFrequency = "month" | "year";
|
||||
|
||||
export type IPaymentProductPrice = {
|
||||
currency: string;
|
||||
id: string;
|
||||
product: string;
|
||||
recurring: TBillingFrequency;
|
||||
unit_amount: number;
|
||||
workspace_amount: number;
|
||||
};
|
||||
|
||||
export type TProductSubscriptionType = "FREE" | "ONE" | "PRO" | "BUSINESS" | "ENTERPRISE";
|
||||
|
||||
export type IPaymentProduct = {
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
type: Omit<TProductSubscriptionType, "FREE">;
|
||||
payment_quantity: number;
|
||||
prices: IPaymentProductPrice[];
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
export type TSubscriptionPrice = {
|
||||
key: string;
|
||||
id: string | undefined;
|
||||
currency: string;
|
||||
price: number;
|
||||
recurring: TBillingFrequency;
|
||||
};
|
||||
|
||||
export type TProductBillingFrequency = {
|
||||
[key in EProductSubscriptionEnum]: TBillingFrequency | undefined;
|
||||
};
|
||||
Vendored
+8
@@ -24,3 +24,11 @@ export interface IStateLite {
|
||||
export interface IStateResponse {
|
||||
[key: string]: IState[];
|
||||
}
|
||||
|
||||
export type TStateOperationsCallbacks = {
|
||||
createState: (data: Partial<IState>) => Promise<IState>;
|
||||
updateState: (stateId: string, data: Partial<IState>) => Promise<IState | undefined>;
|
||||
deleteState: (stateId: string) => Promise<void>;
|
||||
moveStatePosition: (stateId: string, data: Partial<IState>) => Promise<void>;
|
||||
markStateAsDefault: (stateId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { FC } from "react";
|
||||
import { DropdownIcon } from "../icons";
|
||||
import { cn } from "../../helpers";
|
||||
import { DropdownIcon } from "../icons";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
hideChevron?: boolean;
|
||||
indicatorElement?: React.ReactNode;
|
||||
actionItemElement?: React.ReactNode;
|
||||
|
||||
@@ -42,7 +42,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||
<div className={cn("max-h-48 space-y-1 overflow-y-scroll", !disableSearch && "mt-2")}>
|
||||
<>
|
||||
{options ? (
|
||||
options.length > 0 ? (
|
||||
@@ -50,6 +50,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
|
||||
<Combobox.Option
|
||||
key={keyExtractor(option)}
|
||||
value={keyExtractor(option)}
|
||||
disabled={option.disabled}
|
||||
className={({ active, selected }) =>
|
||||
cn(
|
||||
"flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
|
||||
@@ -66,7 +67,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{renderItem ? (
|
||||
<>{renderItem({ value: keyExtractor(option), selected })}</>
|
||||
<>{renderItem({ value: keyExtractor(option), selected, disabled: option.disabled })}</>
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-grow truncate">{option.value}</span>
|
||||
|
||||
+14
-3
@@ -27,7 +27,15 @@ export interface IDropdown {
|
||||
queryArray?: string[];
|
||||
sortByKey?: string;
|
||||
firstItem?: (optionValue: string) => boolean;
|
||||
renderItem?: ({ value, selected }: { value: string; selected: boolean }) => React.ReactNode;
|
||||
renderItem?: ({
|
||||
value,
|
||||
selected,
|
||||
disabled,
|
||||
}: {
|
||||
value: string;
|
||||
selected: boolean;
|
||||
disabled?: boolean;
|
||||
}) => React.ReactNode;
|
||||
loader?: React.ReactNode;
|
||||
disableSorting?: boolean;
|
||||
}
|
||||
@@ -35,7 +43,8 @@ export interface IDropdown {
|
||||
export interface TDropdownOption {
|
||||
data: any;
|
||||
value: string;
|
||||
className?: ({ active, selected }: { active: boolean; selected: boolean }) => string;
|
||||
className?: ({ active, selected }: { active: boolean; selected?: boolean }) => string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IMultiSelectDropdown extends IDropdown {
|
||||
@@ -82,7 +91,9 @@ export interface IDropdownOptions {
|
||||
handleClose?: () => void;
|
||||
|
||||
keyExtractor: (option: TDropdownOption) => string;
|
||||
renderItem: (({ value, selected }: { value: string; selected: boolean }) => React.ReactNode) | undefined;
|
||||
renderItem:
|
||||
| (({ value, selected, disabled }: { value: string; selected: boolean; disabled?: boolean }) => React.ReactNode)
|
||||
| undefined;
|
||||
options: TDropdownOption[] | undefined;
|
||||
loader?: React.ReactNode;
|
||||
isMobile?: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// FIXME: fix this!!!
|
||||
import { Placement } from "@blueprintjs/popover2";
|
||||
import { ICustomSearchSelectOption } from "@plane/types";
|
||||
|
||||
export interface IDropdownProps {
|
||||
customButtonClassName?: string;
|
||||
@@ -44,15 +45,7 @@ interface CustomSearchSelectProps {
|
||||
onChange: any;
|
||||
onClose?: () => void;
|
||||
noResultsMessage?: string;
|
||||
options:
|
||||
| {
|
||||
value: any;
|
||||
query: string;
|
||||
content: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | React.ReactNode;
|
||||
}[]
|
||||
| undefined;
|
||||
options?: ICustomSearchSelectOption[];
|
||||
}
|
||||
|
||||
interface SingleValueProps {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { CompleteOrEmpty } from "@plane/types";
|
||||
|
||||
// Support email can be configured by the application
|
||||
export const getSupportEmail = (defaultEmail: string = ""): string => defaultEmail;
|
||||
@@ -39,3 +40,21 @@ export const partitionValidIds = (ids: string[], validIds: string[]): { valid: s
|
||||
|
||||
return { valid, invalid };
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if an object is complete (has properties) rather than empty.
|
||||
* This helps TypeScript narrow the type from CompleteOrEmpty<T> to T.
|
||||
*
|
||||
* @param obj The object to check, typed as CompleteOrEmpty<T>
|
||||
* @returns A boolean indicating if the object is complete (true) or empty (false)
|
||||
*/
|
||||
export const isComplete = <T>(obj: CompleteOrEmpty<T>): obj is T => {
|
||||
// Check if object is not null or undefined
|
||||
if (obj == null) return false;
|
||||
|
||||
// Check if it's an object
|
||||
if (typeof obj !== "object") return false;
|
||||
|
||||
// Check if it has any own properties
|
||||
return Object.keys(obj).length > 0;
|
||||
};
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from "./string";
|
||||
export * from "./theme";
|
||||
export * from "./workspace";
|
||||
export * from "./work-item";
|
||||
export * from "./subscription";
|
||||
|
||||
@@ -55,13 +55,6 @@ export const createSimilarString = (str: string) => {
|
||||
return shuffled;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Copies full URL (origin + path) to clipboard
|
||||
* @param {string} path - URL path to copy
|
||||
* @returns {Promise<void>} Promise that resolves when copying is complete
|
||||
* @example
|
||||
* await copyUrlToClipboard("issues/123") // copies "https://example.com/issues/123"
|
||||
*/
|
||||
/**
|
||||
* @description Copies text to clipboard
|
||||
* @param {string} text - Text to copy
|
||||
@@ -86,8 +79,11 @@ export const copyTextToClipboard = async (text: string): Promise<void> => {
|
||||
* await copyUrlToClipboard("issues/123") // copies "https://example.com/issues/123"
|
||||
*/
|
||||
export const copyUrlToClipboard = async (path: string) => {
|
||||
const originUrl = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
await copyTextToClipboard(`${originUrl}/${path}`);
|
||||
// get origin or default to empty string if not in browser
|
||||
const originUrl = typeof window !== "undefined" ? window.location.origin : "";
|
||||
// create URL object and ensure proper path formatting
|
||||
const url = new URL(path, originUrl);
|
||||
await copyTextToClipboard(url.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import orderBy from "lodash/orderBy";
|
||||
// plane imports
|
||||
import { EProductSubscriptionEnum } from "@plane/constants";
|
||||
import { IPaymentProduct, TProductSubscriptionType, TSubscriptionPrice } from "@plane/types";
|
||||
|
||||
/**
|
||||
* Calculates the yearly discount percentage when switching from monthly to yearly billing
|
||||
* @param monthlyPrice - The monthly subscription price
|
||||
* @param yearlyPricePerMonth - The monthly equivalent price when billed yearly
|
||||
* @returns The discount percentage as a whole number (floored)
|
||||
*/
|
||||
export const calculateYearlyDiscount = (monthlyPrice: number, yearlyPricePerMonth: number): number => {
|
||||
const monthlyCost = monthlyPrice * 12;
|
||||
const yearlyCost = yearlyPricePerMonth * 12;
|
||||
const amountSaved = monthlyCost - yearlyCost;
|
||||
const discountPercentage = (amountSaved / monthlyCost) * 100;
|
||||
return Math.floor(discountPercentage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the display name for a subscription plan variant
|
||||
* @param planVariant - The subscription plan variant enum
|
||||
* @returns The human-readable name of the plan
|
||||
*/
|
||||
export const getSubscriptionName = (planVariant: EProductSubscriptionEnum): string => {
|
||||
switch (planVariant) {
|
||||
case EProductSubscriptionEnum.FREE:
|
||||
return "Free";
|
||||
case EProductSubscriptionEnum.ONE:
|
||||
return "One";
|
||||
case EProductSubscriptionEnum.PRO:
|
||||
return "Pro";
|
||||
case EProductSubscriptionEnum.BUSINESS:
|
||||
return "Business";
|
||||
case EProductSubscriptionEnum.ENTERPRISE:
|
||||
return "Enterprise";
|
||||
default:
|
||||
return "--";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the base subscription name for upgrade/downgrade paths
|
||||
* @param planVariant - The current subscription plan variant
|
||||
* @param isSelfHosted - Whether the instance is self-hosted / community
|
||||
* @returns The name of the base subscription plan
|
||||
*
|
||||
* @remarks
|
||||
* - For self-hosted / community instances, the upgrade path differs from cloud instances
|
||||
* - Returns the immediate lower tier subscription name
|
||||
*/
|
||||
export const getBaseSubscriptionName = (planVariant: TProductSubscriptionType, isSelfHosted: boolean): string => {
|
||||
switch (planVariant) {
|
||||
case EProductSubscriptionEnum.ONE:
|
||||
return getSubscriptionName(EProductSubscriptionEnum.FREE);
|
||||
case EProductSubscriptionEnum.PRO:
|
||||
return isSelfHosted
|
||||
? getSubscriptionName(EProductSubscriptionEnum.ONE)
|
||||
: getSubscriptionName(EProductSubscriptionEnum.FREE);
|
||||
case EProductSubscriptionEnum.BUSINESS:
|
||||
return getSubscriptionName(EProductSubscriptionEnum.PRO);
|
||||
case EProductSubscriptionEnum.ENTERPRISE:
|
||||
return getSubscriptionName(EProductSubscriptionEnum.BUSINESS);
|
||||
default:
|
||||
return "--";
|
||||
}
|
||||
};
|
||||
|
||||
export type TSubscriptionPriceDetail = {
|
||||
monthlyPriceDetails: TSubscriptionPrice;
|
||||
yearlyPriceDetails: TSubscriptionPrice;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the price details for a subscription product
|
||||
* @param product - The payment product to get price details for
|
||||
* @returns Array of price details for monthly and yearly plans
|
||||
*/
|
||||
export const getSubscriptionPriceDetails = (product: IPaymentProduct | undefined): TSubscriptionPriceDetail => {
|
||||
const productPrices = product?.prices || [];
|
||||
const monthlyPriceDetails = orderBy(productPrices, ["recurring"], ["desc"])?.find(
|
||||
(price) => price.recurring === "month"
|
||||
);
|
||||
const monthlyPriceAmount = Number(((monthlyPriceDetails?.unit_amount || 0) / 100).toFixed(2));
|
||||
const yearlyPriceDetails = orderBy(productPrices, ["recurring"], ["desc"])?.find(
|
||||
(price) => price.recurring === "year"
|
||||
);
|
||||
const yearlyPriceAmount = Number(((yearlyPriceDetails?.unit_amount || 0) / 1200).toFixed(2));
|
||||
|
||||
return {
|
||||
monthlyPriceDetails: {
|
||||
key: "monthly",
|
||||
id: monthlyPriceDetails?.id,
|
||||
currency: "$",
|
||||
price: monthlyPriceAmount,
|
||||
recurring: "month",
|
||||
},
|
||||
yearlyPriceDetails: {
|
||||
key: "yearly",
|
||||
id: yearlyPriceDetails?.id,
|
||||
currency: "$",
|
||||
price: yearlyPriceAmount,
|
||||
recurring: "year",
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -8,13 +8,12 @@ import { Plus, Search } from "lucide-react";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn, copyUrlToClipboard } from "@plane/utils";
|
||||
// components
|
||||
import { CreateProjectModal } from "@/components/project";
|
||||
import { SidebarProjectsListItem } from "@/components/workspace";
|
||||
// hooks
|
||||
import { orderJoinedProjects } from "@/helpers/project.helper";
|
||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
import { useAppTheme, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
|
||||
import { TProject } from "@/plane-web/types";
|
||||
|
||||
+61
-38
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
@@ -18,12 +18,18 @@ import {
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { CycleQuickActions } from "@/components/cycles";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@@ -69,6 +75,8 @@ const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
|
||||
};
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
// refs
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
// states
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
// router
|
||||
@@ -159,6 +167,25 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
const switcherOptions = currentProjectCycleIds
|
||||
?.map((id) => {
|
||||
const _cycle = id === cycleId ? cycleDetails : getCycleById(id);
|
||||
if (!_cycle) return;
|
||||
const cycleLink = `/${workspaceSlug}/projects/${projectId}/cycles/${_cycle.id}`;
|
||||
return {
|
||||
value: _cycle.id,
|
||||
query: _cycle.name,
|
||||
content: (
|
||||
<Link href={cycleLink}>
|
||||
<SwitcherLabel name={_cycle.name} LabelIcon={ContrastIcon} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
const workItemsCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
return (
|
||||
@@ -201,33 +228,29 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
component={
|
||||
<CustomMenu
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
value={cycleId}
|
||||
onChange={() => {}}
|
||||
label={
|
||||
<>
|
||||
<ContrastIcon className="h-3 w-3" />
|
||||
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
|
||||
<p className="truncate">{cycleDetails?.name && cycleDetails.name}</p>
|
||||
{issuesCount && issuesCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`There are ${issuesCount} ${
|
||||
issuesCount > 1 ? "work items" : "work item"
|
||||
} in this cycle`}
|
||||
position="bottom"
|
||||
>
|
||||
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
||||
{issuesCount}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
<div className="flex items-center gap-1">
|
||||
<SwitcherLabel name={cycleDetails?.name} LabelIcon={ContrastIcon} />
|
||||
{workItemsCount && workItemsCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`There are ${workItemsCount} ${
|
||||
workItemsCount > 1 ? "work items" : "work item"
|
||||
} in this cycle`}
|
||||
position="bottom"
|
||||
>
|
||||
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
||||
{workItemsCount}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
className="ml-1.5 flex-shrink-0 truncate"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{currentProjectCycleIds?.map((cycleId) => <CycleDropdownOption key={cycleId} cycleId={cycleId} />)}
|
||||
</CustomMenu>
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
@@ -302,19 +325,19 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||
className="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<ArrowRight className={`h-4 w-4 duration-300 ${isSidebarCollapsed ? "-rotate-180" : ""}`} />
|
||||
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
||||
</button>
|
||||
<CycleQuickActions
|
||||
parentRef={parentRef}
|
||||
cycleId={cycleId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
customClassName="flex-shrink-0 flex items-center justify-center size-6 bg-custom-background-80/70 rounded"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 md:hidden"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
||||
</button>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
</>
|
||||
|
||||
+61
-60
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ArrowRight, PanelRight } from "lucide-react";
|
||||
import { PanelRight } from "lucide-react";
|
||||
// plane constants
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
@@ -16,17 +16,22 @@ import {
|
||||
EUserPermissionsLevel,
|
||||
} from "@plane/constants";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// helpers
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
useEventTracker,
|
||||
@@ -46,30 +51,9 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
|
||||
const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { getModuleById } = useModule();
|
||||
// derived values
|
||||
const moduleDetail = getModuleById(moduleId);
|
||||
|
||||
if (!moduleDetail) return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem key={moduleDetail.id}>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/modules/${moduleDetail.id}`}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
<DiceIcon className="h-3 w-3" />
|
||||
{truncateText(moduleDetail.name, 40)}
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
// refs
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
// states
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
// router
|
||||
@@ -155,7 +139,24 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
const workItemsCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
const switcherOptions = projectModuleIds
|
||||
?.map((id) => {
|
||||
const _module = id === moduleId ? moduleDetails : getModuleById(id);
|
||||
if (!_module) return;
|
||||
const moduleLink = `/${workspaceSlug}/projects/${projectId}/modules/${_module.id}`;
|
||||
return {
|
||||
value: _module.id,
|
||||
query: _module.name,
|
||||
content: (
|
||||
<Link href={moduleLink}>
|
||||
<SwitcherLabel name={_module.name} LabelIcon={DiceIcon} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -196,33 +197,29 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
component={
|
||||
<CustomMenu
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
label={
|
||||
<>
|
||||
<DiceIcon className="h-3 w-3" />
|
||||
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
|
||||
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
|
||||
{issuesCount && issuesCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`There are ${issuesCount} ${
|
||||
issuesCount > 1 ? "work items" : "work item"
|
||||
} in this module`}
|
||||
position="bottom"
|
||||
>
|
||||
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
||||
{issuesCount}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
<div className="flex items-center gap-1">
|
||||
<SwitcherLabel name={moduleDetails?.name} LabelIcon={DiceIcon} />
|
||||
{workItemsCount && workItemsCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`There are ${workItemsCount} ${
|
||||
workItemsCount > 1 ? "work items" : "work item"
|
||||
} in this module`}
|
||||
position="bottom"
|
||||
>
|
||||
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
||||
{workItemsCount}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
className="ml-1.5 flex-shrink-0"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)}
|
||||
</CustomMenu>
|
||||
value={moduleId}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
@@ -302,14 +299,18 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||
className="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<ArrowRight className={`hidden h-4 w-4 duration-300 md:block ${isSidebarCollapsed ? "-rotate-180" : ""}`} />
|
||||
<PanelRight
|
||||
className={cn("block h-4 w-4 md:hidden", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")}
|
||||
/>
|
||||
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
||||
</button>
|
||||
<ModuleQuickActions
|
||||
parentRef={parentRef}
|
||||
moduleId={moduleId?.toString()}
|
||||
projectId={projectId.toString()}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
customClassName="flex-shrink-0 flex items-center justify-center size-6 bg-custom-background-80/70 rounded"
|
||||
/>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
</>
|
||||
|
||||
+54
-91
@@ -1,27 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FileText } from "lucide-react";
|
||||
import { ArchiveIcon, Earth, FileText, Lock } from "lucide-react";
|
||||
// types
|
||||
import { TLogoProps } from "@plane/types";
|
||||
import { EPageAccess } from "@plane/constants";
|
||||
import { ICustomSearchSelectOption, TPage } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, Tooltip, setToast, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { PageEditInformationPopover } from "@/components/pages";
|
||||
// helpers
|
||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||
import { getPageName } from "@/helpers/page.helper";
|
||||
// hooks
|
||||
import { getPageName } from "@/helpers/page.helper";
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePage } from "@/plane-web/hooks/store";
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||
|
||||
const PageAccessIcon = (page: TPage) => (
|
||||
<div>
|
||||
{page.archived_at ? (
|
||||
<ArchiveIcon className="h-2.5 w-2.5 text-custom-text-300" />
|
||||
) : page.access === EPageAccess.PUBLIC ? (
|
||||
<Earth className="h-2.5 w-2.5 text-custom-text-300" />
|
||||
) : (
|
||||
<Lock className="h-2.5 w-2.5 text-custom-text-300" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export interface IPagesHeaderProps {
|
||||
showButton?: boolean;
|
||||
@@ -29,42 +39,39 @@ export interface IPagesHeaderProps {
|
||||
|
||||
export const PageDetailsHeader = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, pageId } = useParams();
|
||||
// state
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { workspaceSlug, pageId, projectId } = useParams();
|
||||
// store hooks
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const page = usePage({
|
||||
pageId: pageId?.toString() ?? "",
|
||||
storeType: EPageStoreType.PROJECT,
|
||||
});
|
||||
if (!page) return null;
|
||||
const { getPageById, getCurrentProjectPageIds } = usePageStore(EPageStoreType.PROJECT);
|
||||
// derived values
|
||||
const { name, logo_props, updatePageLogo, isContentEditable } = page;
|
||||
// use platform
|
||||
const { isMobile } = usePlatformOS();
|
||||
const projectPageIds = getCurrentProjectPageIds(projectId?.toString());
|
||||
|
||||
const handlePageLogoUpdate = async (data: TLogoProps) => {
|
||||
if (data) {
|
||||
updatePageLogo(data)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Logo Updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
if (!page) return null;
|
||||
const switcherOptions = projectPageIds
|
||||
.map((id) => {
|
||||
const _page = id === pageId ? page : getPageById(id);
|
||||
if (!_page) return;
|
||||
const pageLink = `/${workspaceSlug}/projects/${projectId}/pages/${_page.id}`;
|
||||
return {
|
||||
value: _page.id,
|
||||
query: _page.name,
|
||||
content: (
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
<Link href={pageLink} className="flex gap-2 items-center justify-between w-full">
|
||||
<SwitcherLabel logo_props={_page.logo_props} name={getPageName(_page.name)} LabelIcon={FileText} />
|
||||
</Link>
|
||||
<PageAccessIcon {..._page} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
const pageTitle = getPageName(name);
|
||||
if (!page) return null;
|
||||
|
||||
return (
|
||||
<Header>
|
||||
@@ -99,60 +106,16 @@ export const PageDetailsHeader = observer(() => {
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
|
||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden">
|
||||
<EmojiIconPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={(val: boolean) => setIsOpen(val)}
|
||||
className="flex items-center justify-center"
|
||||
buttonClassName="flex items-center justify-center"
|
||||
label={
|
||||
<>
|
||||
{logo_props?.in_use ? (
|
||||
<Logo logo={logo_props} size={16} type="lucide" />
|
||||
) : (
|
||||
<FileText className="h-4 w-4 text-custom-text-300" />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onChange={(val) => {
|
||||
let logoValue = {};
|
||||
|
||||
if (val?.type === "emoji")
|
||||
logoValue = {
|
||||
value: convertHexEmojiToDecimal(val.value.unified),
|
||||
url: val.value.imageUrl,
|
||||
};
|
||||
else if (val?.type === "icon") logoValue = val.value;
|
||||
|
||||
handlePageLogoUpdate({
|
||||
in_use: val?.type,
|
||||
[val?.type]: logoValue,
|
||||
}).finally(() => setIsOpen(false));
|
||||
}}
|
||||
defaultIconColor={
|
||||
logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined
|
||||
}
|
||||
defaultOpen={
|
||||
logo_props?.in_use && logo_props?.in_use === "emoji"
|
||||
? EmojiIconPickerTypes.EMOJI
|
||||
: EmojiIconPickerTypes.ICON
|
||||
}
|
||||
disabled={!isContentEditable}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip tooltipContent={pageTitle} position="bottom" isMobile={isMobile}>
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">
|
||||
{pageTitle}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
type="component"
|
||||
component={
|
||||
<CustomSearchSelect
|
||||
value={pageId}
|
||||
options={switcherOptions}
|
||||
label={
|
||||
<SwitcherLabel logo_props={page.logo_props} name={getPageName(page.name)} LabelIcon={FileText} />
|
||||
}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
+11
@@ -1,11 +1,22 @@
|
||||
"use client";
|
||||
|
||||
// component
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
|
||||
// local components
|
||||
import { PageDetailsHeader } from "./header";
|
||||
|
||||
export default function ProjectPageDetailsLayout({ children }: { children: React.ReactNode }) {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const { fetchPagesList } = usePageStore(EPageStoreType.PROJECT);
|
||||
// fetching pages list
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_PAGES_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchPagesList(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<PageDetailsHeader />} />
|
||||
|
||||
+41
-50
@@ -16,17 +16,21 @@ import {
|
||||
EUserPermissionsLevel,
|
||||
} from "@plane/constants";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, Tooltip, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { ViewQuickActions } from "@/components/views";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
useCommandPalette,
|
||||
@@ -143,6 +147,23 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
if (!viewDetails) return;
|
||||
|
||||
const switcherOptions = projectViewIds
|
||||
?.map((id) => {
|
||||
const _view = id === viewId ? viewDetails : getViewById(id);
|
||||
if (!_view) return;
|
||||
const viewLink = `/${workspaceSlug}/projects/${projectId}/views/${_view.id}`;
|
||||
return {
|
||||
value: _view.id,
|
||||
query: _view.name,
|
||||
content: (
|
||||
<Link href={viewLink}>
|
||||
<SwitcherLabel logo_props={_view.logo_props} name={_view.name} LabelIcon={Layers} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
@@ -161,42 +182,12 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
component={
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
{viewDetails?.logo_props?.in_use ? (
|
||||
<Logo logo={viewDetails.logo_props} size={12} type="lucide" />
|
||||
) : (
|
||||
<Layers height={12} width={12} />
|
||||
)}
|
||||
{viewDetails?.name && truncateText(viewDetails.name, 40)}
|
||||
</>
|
||||
}
|
||||
className="ml-1.5"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{projectViewIds?.map((viewId) => {
|
||||
const view = getViewById(viewId);
|
||||
|
||||
if (!view) return;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem key={viewId}>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/views/${viewId}`}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
{view?.logo_props?.in_use ? (
|
||||
<Logo logo={view.logo_props} size={12} type="lucide" />
|
||||
) : (
|
||||
<Layers height={12} width={12} />
|
||||
)}
|
||||
{truncateText(view.name, 40)}
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
value={viewId}
|
||||
label={<SwitcherLabel logo_props={viewDetails.logo_props} name={viewDetails.name} LabelIcon={Layers} />}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
@@ -210,17 +201,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<div className="hidden md:block">
|
||||
<ViewQuickActions
|
||||
parentRef={parentRef}
|
||||
projectId={projectId.toString()}
|
||||
view={viewDetails}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
<Header.RightItem className="items-center">
|
||||
{!viewDetails?.is_locked ? (
|
||||
<>
|
||||
<LayoutSelection
|
||||
@@ -287,6 +269,15 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div className="hidden md:block">
|
||||
<ViewQuickActions
|
||||
parentRef={parentRef}
|
||||
customClassName="flex-shrink-0 flex items-center justify-center size-6 bg-custom-background-80/70 rounded"
|
||||
projectId={projectId.toString()}
|
||||
view={viewDetails}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
</div>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ export const MobileWorkspaceSettingsTabs = observer(() => {
|
||||
<div className="flex-shrink-0 md:hidden sticky inset-0 flex overflow-x-auto bg-custom-background-100 z-10">
|
||||
{WORKSPACE_SETTINGS_LINKS.map(
|
||||
(item, index) =>
|
||||
shouldRenderSettingLink(item.key) &&
|
||||
shouldRenderSettingLink(workspaceSlug.toString(), item.key) &&
|
||||
allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
|
||||
<div
|
||||
className={`${
|
||||
|
||||
@@ -28,7 +28,7 @@ export const WorkspaceSettingsSidebar = observer(() => {
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{WORKSPACE_SETTINGS_LINKS.map(
|
||||
(link) =>
|
||||
shouldRenderSettingLink(link.key) &&
|
||||
shouldRenderSettingLink(workspaceSlug.toString(), link.key) &&
|
||||
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
|
||||
<Link key={link.key} href={`/${workspaceSlug}${link.href}`}>
|
||||
<SidebarNavItem
|
||||
|
||||
@@ -17,7 +17,7 @@ import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { checkEmailValidity } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
import { useEventTracker, useInstance } from "@/hooks/store";
|
||||
import useTimer from "@/hooks/use-timer";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
@@ -48,6 +48,7 @@ const ForgotPasswordPage = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { config } = useInstance();
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
// timer
|
||||
@@ -93,6 +94,9 @@ const ForgotPasswordPage = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
// derived values
|
||||
const enableSignUpConfig = config?.enable_signup ?? false;
|
||||
|
||||
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
|
||||
|
||||
return (
|
||||
@@ -101,39 +105,41 @@ const ForgotPasswordPage = observer(() => {
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||
className="w-full h-full object-cover"
|
||||
className="object-cover w-full h-full"
|
||||
alt="Plane background pattern"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<div className="relative z-10 flex flex-col w-screen h-screen overflow-hidden overflow-y-auto">
|
||||
<div className="container relative flex items-center justify-between flex-shrink-0 min-w-full px-10 pb-4 transition-all lg:px-20 xl:px-36">
|
||||
<div className="flex items-center py-10 gap-x-2">
|
||||
<Link href={`/`} className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
|
||||
{t("auth.common.new_to_plane")}
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
||||
className="font-semibold text-custom-primary-100 hover:underline"
|
||||
>
|
||||
{t("auth.common.create_account")}
|
||||
</Link>
|
||||
</div>
|
||||
{enableSignUpConfig && (
|
||||
<div className="flex flex-col items-end text-sm font-medium text-center sm:items-center sm:gap-2 sm:flex-row text-onboarding-text-300">
|
||||
{t("auth.common.new_to_plane")}
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
||||
className="font-semibold text-custom-primary-100 hover:underline"
|
||||
>
|
||||
{t("auth.common.create_account")}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 lg:pt-28 transition-all">
|
||||
<div className="container flex-grow max-w-lg px-10 py-10 mx-auto transition-all lg:max-w-md lg:px-5 lg:pt-28">
|
||||
<div className="relative flex flex-col space-y-6">
|
||||
<div className="text-center space-y-1 py-4">
|
||||
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
|
||||
<div className="py-4 space-y-1 text-center">
|
||||
<h3 className="flex justify-center gap-4 text-3xl font-bold text-onboarding-text-100">
|
||||
{t("auth.forgot_password.title")}
|
||||
</h3>
|
||||
<p className="font-medium text-onboarding-text-400">{t("auth.forgot_password.description")}</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(handleForgotPassword)} className="mt-5 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email">
|
||||
{t("auth.common.email.label")}
|
||||
</label>
|
||||
<Controller
|
||||
@@ -160,7 +166,7 @@ const ForgotPasswordPage = observer(() => {
|
||||
)}
|
||||
/>
|
||||
{resendTimerCode > 0 && (
|
||||
<p className="flex w-full items-start px-1 gap-1 text-xs font-medium text-green-700">
|
||||
<p className="flex items-start w-full gap-1 px-1 text-xs font-medium text-green-700">
|
||||
<CircleCheck height={12} width={12} className="mt-0.5" />
|
||||
{t("auth.forgot_password.email_sent")}
|
||||
</p>
|
||||
|
||||
+21
-15
@@ -15,7 +15,7 @@ import { PageHead } from "@/components/core";
|
||||
// helpers
|
||||
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
import { useEventTracker, useInstance } from "@/hooks/store";
|
||||
// layouts
|
||||
import DefaultLayout from "@/layouts/default-layout";
|
||||
// wrappers
|
||||
@@ -32,6 +32,10 @@ const HomePage = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
// store
|
||||
const { config } = useInstance();
|
||||
// derived values
|
||||
const enableSignUpConfig = config?.enable_signup ?? false;
|
||||
|
||||
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
|
||||
|
||||
@@ -44,27 +48,29 @@ const HomePage = observer(() => {
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||
className="w-full h-full object-cover"
|
||||
className="object-cover w-full h-full"
|
||||
alt="Plane background pattern"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<div className="relative z-10 flex flex-col w-screen h-screen overflow-hidden overflow-y-auto">
|
||||
<div className="container relative flex items-center justify-between flex-shrink-0 min-w-full px-10 pb-4 transition-all lg:px-20 xl:px-36">
|
||||
<div className="flex items-center py-10 gap-x-2">
|
||||
<Link href={`/`} className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
|
||||
{t("auth.common.new_to_plane")}
|
||||
<Link
|
||||
href="/sign-up"
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
||||
className="font-semibold text-custom-primary-100 hover:underline"
|
||||
>
|
||||
{t("auth.common.create_account")}
|
||||
</Link>
|
||||
</div>
|
||||
{enableSignUpConfig && (
|
||||
<div className="flex flex-col items-end text-sm font-medium text-center sm:items-center sm:gap-2 sm:flex-row text-onboarding-text-300">
|
||||
{t("auth.common.new_to_plane")}
|
||||
<Link
|
||||
href="/sign-up"
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
||||
className="font-semibold text-custom-primary-100 hover:underline"
|
||||
>
|
||||
{t("auth.common.create_account")}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-center flex-grow container h-[100vh-60px] mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 transition-all">
|
||||
<AuthRoot authMode={EAuthModes.SIGN_IN} />
|
||||
|
||||
@@ -50,7 +50,7 @@ const WorkspaceInvitationPage = observer(() => {
|
||||
})
|
||||
.then(() => {
|
||||
if (email === currentUser?.email) {
|
||||
router.push("/invitations");
|
||||
router.push(`/${invitationDetail.workspace.slug}`);
|
||||
} else {
|
||||
router.push(`/?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueServiceType, EIssuesStoreType } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
// components
|
||||
import { BulkDeleteIssuesModal } from "@/components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// constants
|
||||
// hooks
|
||||
import { useCommandPalette, useIssueDetail, useUser } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
|
||||
export type TIssueLevelModalsProps = {
|
||||
projectId: string | undefined;
|
||||
@@ -26,9 +28,10 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const {
|
||||
issues: { removeIssue },
|
||||
} = useIssuesStore();
|
||||
|
||||
const { removeIssue: removeEpic } = useIssuesActions(EIssuesStoreType.EPIC);
|
||||
const { removeIssue: removeWorkItem } = useIssuesActions(EIssuesStoreType.PROJECT);
|
||||
|
||||
const {
|
||||
isCreateIssueModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
@@ -40,24 +43,51 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
|
||||
// derived values
|
||||
const issueDetails = issueId ? getIssueById(issueId) : undefined;
|
||||
const isDraftIssue = pathname?.includes("draft-issues") || false;
|
||||
const { fetchSubIssues: fetchSubWorkItems } = useIssueDetail();
|
||||
const { fetchSubIssues: fetchEpicSubWorkItems } = useIssueDetail(EIssueServiceType.EPICS);
|
||||
|
||||
const handleDeleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const isEpic = issueDetails?.is_epic;
|
||||
const deleteAction = isEpic ? removeEpic : removeWorkItem;
|
||||
const redirectPath = `/${workspaceSlug}/projects/${projectId}/${isEpic ? "epics" : "issues"}`;
|
||||
|
||||
await deleteAction(projectId, issueId);
|
||||
router.push(redirectPath);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete issue:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateIssueSubmit = async (newIssue: TIssue) => {
|
||||
if (!workspaceSlug || !newIssue.project_id || !newIssue.id || newIssue.parent_id !== issueDetails?.id) return;
|
||||
|
||||
const fetchAction = issueDetails?.is_epic ? fetchEpicSubWorkItems : fetchSubWorkItems;
|
||||
await fetchAction(workspaceSlug?.toString(), newIssue.project_id, issueDetails.id);
|
||||
};
|
||||
|
||||
const getCreateIssueModalData = () => {
|
||||
if (cycleId) return { cycle_id: cycleId.toString() };
|
||||
if (moduleId) return { module_ids: [moduleId.toString()] };
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isCreateIssueModalOpen}
|
||||
onClose={() => toggleCreateIssueModal(false)}
|
||||
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined}
|
||||
data={getCreateIssueModalData()}
|
||||
isDraft={isDraftIssue}
|
||||
onSubmit={handleCreateIssueSubmit}
|
||||
/>
|
||||
{workspaceSlug && projectId && issueId && issueDetails && (
|
||||
<DeleteIssueModal
|
||||
handleClose={() => toggleDeleteIssueModal(false)}
|
||||
isOpen={isDeleteIssueModalOpen}
|
||||
data={issueDetails}
|
||||
onSubmit={async () => {
|
||||
await removeIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString());
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
|
||||
}}
|
||||
onSubmit={() => handleDeleteIssue(workspaceSlug.toString(), projectId?.toString(), issueId?.toString())}
|
||||
isEpic={issueDetails?.is_epic}
|
||||
/>
|
||||
)}
|
||||
<BulkDeleteIssuesModal
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user