Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f56caa414 | ||
|
|
ca02b0d10e | ||
|
|
eaf8ce0ed2 | ||
|
|
a4450d34b4 | ||
|
|
7f19caf5dc | ||
|
|
3ec71d6607 | ||
|
|
571c3f6212 | ||
|
|
9a3fb8b9ee | ||
|
|
385be31ff1 | ||
|
|
2e022a4ed7 | ||
|
|
0e110f3fd8 | ||
|
|
9093b41132 | ||
|
|
ad2dc6759a | ||
|
|
e2550e0b2d | ||
|
|
b016ed78cf | ||
|
|
c429ca7b36 | ||
|
|
ee22dbba1b | ||
|
|
f4a208bd44 | ||
|
|
8edff26ccd | ||
|
|
d08c03f557 | ||
|
|
0b53912295 | ||
|
|
586a320d86 | ||
|
|
8f3a0be177 | ||
|
|
0679e140a2 | ||
|
|
b611f5110f | ||
|
|
0f7bc6979f | ||
|
|
12501d0597 | ||
|
|
3a86fff7c1 | ||
|
|
58a4b45463 | ||
|
|
22836ea03e | ||
|
|
13cc8b0e96 | ||
|
|
9addcde553 | ||
|
|
26a9b7fced | ||
|
|
62dd80874f | ||
|
|
9ae1ce0a9a | ||
|
|
00cc338c07 | ||
|
|
4432be15e4 | ||
|
|
20893c6017 | ||
|
|
95f43a7bb6 | ||
|
|
fd7eedc343 | ||
|
|
d2c9b437f4 | ||
|
|
2f57d0e138 | ||
|
|
59ddc02a31 | ||
|
|
3ac20741d9 | ||
|
|
8ea0772a1b | ||
|
|
bddad8932b | ||
|
|
8acea7f599 | ||
|
|
a908bf9edd | ||
|
|
79fff4744a | ||
|
|
369d927321 | ||
|
|
996d11de12 | ||
|
|
0345336d90 | ||
|
|
75d14e7c3a | ||
|
|
76fdb81249 | ||
|
|
88669af141 | ||
|
|
71dcbd938e | ||
|
|
4060412b18 | ||
|
|
9d715683f7 |
@@ -35,7 +35,6 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "7.51.5",
|
||||
"swr": "^2.2.4",
|
||||
"tailwindcss": "3.3.2",
|
||||
"uuid": "^9.0.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
|
||||
@@ -207,6 +207,7 @@ class IssueSerializer(BaseSerializer):
|
||||
for assignee_id in assignees
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
@@ -224,6 +225,7 @@ class IssueSerializer(BaseSerializer):
|
||||
for label_id in labels
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Time updation occues even when other related models are updated
|
||||
|
||||
@@ -72,6 +72,8 @@ from .issue import (
|
||||
IssueReactionLiteSerializer,
|
||||
IssueAttachmentLiteSerializer,
|
||||
IssueLinkLiteSerializer,
|
||||
IssueVersionDetailSerializer,
|
||||
IssueDescriptionVersionDetailSerializer,
|
||||
)
|
||||
|
||||
from .module import (
|
||||
|
||||
@@ -33,6 +33,8 @@ from plane.db.models import (
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
State,
|
||||
IssueVersion,
|
||||
IssueDescriptionVersion,
|
||||
)
|
||||
|
||||
|
||||
@@ -201,6 +203,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
for user in assignees
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
@@ -218,6 +221,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
for label in labels
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Time updation occues even when other related models are updated
|
||||
@@ -281,10 +285,26 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
)
|
||||
name = serializers.CharField(source="related_issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="related_issue.priority", read_only=True)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
@@ -296,10 +316,26 @@ class RelatedIssueSerializer(BaseSerializer):
|
||||
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
state_id = serializers.UUIDField(source="issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="issue.priority", read_only=True)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
@@ -667,3 +703,64 @@ class IssueSubscriberSerializer(BaseSerializer):
|
||||
model = IssueSubscriber
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "project", "issue"]
|
||||
|
||||
|
||||
class IssueVersionDetailSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueVersion
|
||||
fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"parent",
|
||||
"state",
|
||||
"estimate_point",
|
||||
"name",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"assignees",
|
||||
"sequence_id",
|
||||
"labels",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"archived_at",
|
||||
"is_draft",
|
||||
"external_source",
|
||||
"external_id",
|
||||
"type",
|
||||
"cycle",
|
||||
"modules",
|
||||
"meta",
|
||||
"name",
|
||||
"last_saved_at",
|
||||
"owned_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = ["workspace", "project", "issue"]
|
||||
|
||||
|
||||
class IssueDescriptionVersionDetailSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueDescriptionVersion
|
||||
fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"description_binary",
|
||||
"description_html",
|
||||
"description_stripped",
|
||||
"description_json",
|
||||
"last_saved_at",
|
||||
"owned_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = ["workspace", "project", "issue"]
|
||||
|
||||
@@ -24,6 +24,8 @@ from plane.app.views import (
|
||||
IssueDetailEndpoint,
|
||||
IssueAttachmentV2Endpoint,
|
||||
IssueBulkUpdateDateEndpoint,
|
||||
IssueVersionEndpoint,
|
||||
IssueDescriptionVersionEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -256,4 +258,24 @@ urlpatterns = [
|
||||
IssueBulkUpdateDateEndpoint.as_view(),
|
||||
name="project-issue-dates",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
|
||||
IssueDescriptionVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
|
||||
IssueDescriptionVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -141,6 +141,8 @@ from .issue.sub_issue import SubIssuesEndpoint
|
||||
|
||||
from .issue.subscriber import IssueSubscriberViewSet
|
||||
|
||||
from .issue.version import IssueVersionEndpoint, IssueDescriptionVersionEndpoint
|
||||
|
||||
from .module.base import (
|
||||
ModuleViewSet,
|
||||
ModuleLinkViewSet,
|
||||
|
||||
@@ -132,6 +132,18 @@ class CycleViewSet(BaseViewSet):
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"issue_cycle__issue__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group__in=["backlog", "unstarted", "started"],
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
@@ -214,6 +226,7 @@ class CycleViewSet(BaseViewSet):
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"completed_issues",
|
||||
"pending_issues",
|
||||
"assignee_ids",
|
||||
"status",
|
||||
"version",
|
||||
@@ -245,6 +258,7 @@ class CycleViewSet(BaseViewSet):
|
||||
# meta fields
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"pending_issues",
|
||||
"completed_issues",
|
||||
"assignee_ids",
|
||||
"status",
|
||||
|
||||
@@ -53,10 +53,10 @@ from .. import BaseAPIView
|
||||
def dashboard_overview_stats(self, request, slug):
|
||||
assigned_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
(Q(assignees__in=[request.user]) & Q(issue_assignee__deleted_at__isnull=True)),
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
workspace__slug=slug,
|
||||
assignees__in=[request.user],
|
||||
)
|
||||
.filter(
|
||||
Q(
|
||||
@@ -133,10 +133,13 @@ def dashboard_overview_stats(self, request, slug):
|
||||
|
||||
completed_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[request.user])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
assignees__in=[request.user],
|
||||
state__group="completed",
|
||||
)
|
||||
.filter(
|
||||
@@ -176,10 +179,13 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
# get all the assigned issues
|
||||
assigned_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[request.user])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
assignees__in=[request.user],
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import IssueVersion, IssueDescriptionVersion
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
IssueVersionDetailSerializer,
|
||||
IssueDescriptionVersionDetailSerializer,
|
||||
)
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.utils.global_paginator import paginate
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
|
||||
|
||||
class IssueVersionEndpoint(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, issue_id, pk=None):
|
||||
if pk:
|
||||
issue_version = IssueVersion.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
|
||||
serializer = IssueVersionDetailSerializer(issue_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_versions_queryset = IssueVersion.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id
|
||||
)
|
||||
|
||||
paginated_data = paginate(
|
||||
base_queryset=issue_versions_queryset,
|
||||
queryset=issue_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)
|
||||
|
||||
|
||||
class IssueDescriptionVersionEndpoint(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, issue_id, pk=None):
|
||||
if pk:
|
||||
issue_description_version = IssueDescriptionVersion.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_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=issue_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)
|
||||
@@ -39,9 +39,9 @@ class WorkspaceStickyViewSet(BaseViewSet):
|
||||
)
|
||||
def list(self, request, slug):
|
||||
query = request.query_params.get("query", False)
|
||||
stickies = self.get_queryset()
|
||||
stickies = self.get_queryset().order_by("-sort_order")
|
||||
if query:
|
||||
stickies = stickies.filter(name__icontains=query)
|
||||
stickies = stickies.filter(description_stripped__icontains=query)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
@@ -49,7 +49,7 @@ class WorkspaceStickyViewSet(BaseViewSet):
|
||||
on_results=lambda stickies: StickySerializer(stickies, many=True).data,
|
||||
default_per_page=20,
|
||||
)
|
||||
|
||||
|
||||
@allow_permission(allowed_roles=[], creator=True, model=Sticky, level="WORKSPACE")
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@@ -375,8 +375,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
|
||||
state_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
assignees__in=[user_id],
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
@@ -391,8 +394,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
|
||||
priority_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
assignees__in=[user_id],
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
@@ -426,8 +432,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
|
||||
assigned_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
assignees__in=[user_id],
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
@@ -438,8 +447,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
pending_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
~Q(state__group__in=["completed", "cancelled"]),
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
assignees__in=[user_id],
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
@@ -449,8 +461,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
|
||||
completed_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
assignees__in=[user_id],
|
||||
state__group="completed",
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
|
||||
@@ -5,6 +5,9 @@ from django.db import models
|
||||
# Module imports
|
||||
from .base import BaseModel
|
||||
|
||||
# Third party imports
|
||||
from plane.utils.html_processor import strip_tags
|
||||
|
||||
|
||||
class Sticky(BaseModel):
|
||||
name = models.TextField(null=True, blank=True)
|
||||
@@ -33,6 +36,12 @@ class Sticky(BaseModel):
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
if self._state.adding:
|
||||
# Get the maximum sequence value from the database
|
||||
last_id = Sticky.objects.filter(workspace=self.workspace).aggregate(
|
||||
|
||||
@@ -290,11 +290,12 @@ class InstanceAdminSignInEndpoint(View):
|
||||
# Fetch the user
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# is_active
|
||||
if not user.is_active:
|
||||
# Error out if the user is not present
|
||||
if not user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DEACTIVATED"],
|
||||
error_message="ADMIN_USER_DEACTIVATED",
|
||||
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DOES_NOT_EXIST"],
|
||||
error_message="ADMIN_USER_DOES_NOT_EXIST",
|
||||
payload={"email": email},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
@@ -302,12 +303,11 @@ class InstanceAdminSignInEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Error out if the user is not present
|
||||
if not user:
|
||||
# is_active
|
||||
if not user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DOES_NOT_EXIST"],
|
||||
error_message="ADMIN_USER_DOES_NOT_EXIST",
|
||||
payload={"email": email},
|
||||
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DEACTIVATED"],
|
||||
error_message="ADMIN_USER_DEACTIVATED",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# base requirements
|
||||
|
||||
# django
|
||||
Django==4.2.17
|
||||
Django==4.2.18
|
||||
# rest framework
|
||||
djangorestframework==3.15.2
|
||||
# postgres
|
||||
|
||||
+6
-6
@@ -16,10 +16,10 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@hocuspocus/extension-database": "^2.11.3",
|
||||
"@hocuspocus/extension-logger": "^2.11.3",
|
||||
"@hocuspocus/extension-redis": "^2.13.5",
|
||||
"@hocuspocus/server": "^2.11.3",
|
||||
"@hocuspocus/extension-database": "^2.15.0",
|
||||
"@hocuspocus/extension-logger": "^2.15.0",
|
||||
"@hocuspocus/extension-redis": "^2.15.0",
|
||||
"@hocuspocus/server": "^2.15.0",
|
||||
"@plane/constants": "*",
|
||||
"@plane/editor": "*",
|
||||
"@plane/types": "*",
|
||||
@@ -40,9 +40,9 @@
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^11.2.2",
|
||||
"uuid": "^10.0.0",
|
||||
"y-prosemirror": "^1.2.9",
|
||||
"y-prosemirror": "^1.2.15",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.14"
|
||||
"yjs": "^13.6.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.25.6",
|
||||
|
||||
@@ -2,3 +2,56 @@ export enum E_SORT_ORDER {
|
||||
ASC = "asc",
|
||||
DESC = "desc",
|
||||
}
|
||||
export const DATE_AFTER_FILTER_OPTIONS = [
|
||||
{
|
||||
name: "1 week from now",
|
||||
value: "1_weeks;after;fromnow",
|
||||
},
|
||||
{
|
||||
name: "2 weeks from now",
|
||||
value: "2_weeks;after;fromnow",
|
||||
},
|
||||
{
|
||||
name: "1 month from now",
|
||||
value: "1_months;after;fromnow",
|
||||
},
|
||||
{
|
||||
name: "2 months from now",
|
||||
value: "2_months;after;fromnow",
|
||||
},
|
||||
];
|
||||
|
||||
export const DATE_BEFORE_FILTER_OPTIONS = [
|
||||
{
|
||||
name: "1 week ago",
|
||||
value: "1_weeks;before;fromnow",
|
||||
},
|
||||
{
|
||||
name: "2 weeks ago",
|
||||
value: "2_weeks;before;fromnow",
|
||||
},
|
||||
{
|
||||
name: "1 month ago",
|
||||
i18n_name: "date_filters.1_month_ago",
|
||||
value: "1_months;before;fromnow",
|
||||
},
|
||||
];
|
||||
|
||||
export const PROJECT_CREATED_AT_FILTER_OPTIONS = [
|
||||
{
|
||||
name: "Today",
|
||||
value: "today;custom;custom",
|
||||
},
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: "yesterday;custom;custom",
|
||||
},
|
||||
{
|
||||
name: "Last 7 days",
|
||||
value: "last_7_days;custom;custom",
|
||||
},
|
||||
{
|
||||
name: "Last 30 days",
|
||||
value: "last_30_days;custom;custom",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
|
||||
|
||||
export enum EInboxIssueCurrentTab {
|
||||
OPEN = "open",
|
||||
CLOSED = "closed",
|
||||
}
|
||||
|
||||
export enum EInboxIssueStatus {
|
||||
PENDING = -2,
|
||||
DECLINED = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export const INBOX_STATUS: {
|
||||
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`,
|
||||
},
|
||||
];
|
||||
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
export const INBOX_ISSUE_SORT_BY_OPTIONS = [
|
||||
{
|
||||
key: "asc",
|
||||
i18n_label: "common.sort.asc",
|
||||
},
|
||||
{
|
||||
key: "desc",
|
||||
i18n_label: "common.sort.desc",
|
||||
},
|
||||
];
|
||||
@@ -11,5 +11,12 @@ export * from "./issue";
|
||||
export * from "./metadata";
|
||||
export * from "./state";
|
||||
export * from "./swr";
|
||||
export * from "./tab-indices";
|
||||
export * from "./user";
|
||||
export * from "./workspace";
|
||||
export * from "./stickies";
|
||||
export * from "./project";
|
||||
export * from "./views";
|
||||
export * from "./inbox";
|
||||
export * from "./profile";
|
||||
export * from "./workspace-drafts";
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import { List, Kanban } from "lucide-react";
|
||||
|
||||
export const ALL_ISSUES = "All Issues";
|
||||
|
||||
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
|
||||
|
||||
export type TIssueFilterKeys = "priority" | "state" | "labels";
|
||||
|
||||
export type TIssueLayout =
|
||||
| "list"
|
||||
| "kanban"
|
||||
| "calendar"
|
||||
| "spreadsheet"
|
||||
| "gantt";
|
||||
|
||||
export type TIssueFilterPriorityObject = {
|
||||
key: TIssuePriorities;
|
||||
title: string;
|
||||
className: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
export enum EIssueGroupByToServerOptions {
|
||||
"state" = "state_id",
|
||||
"priority" = "priority",
|
||||
"labels" = "labels__id",
|
||||
"state_detail.group" = "state__group",
|
||||
"assignees" = "assignees__id",
|
||||
"cycle" = "cycle_id",
|
||||
"module" = "issue_module__module_id",
|
||||
"target_date" = "target_date",
|
||||
"project" = "project_id",
|
||||
"created_by" = "created_by",
|
||||
"team_project" = "project_id",
|
||||
}
|
||||
|
||||
export enum EIssueGroupBYServerToProperty {
|
||||
"state_id" = "state_id",
|
||||
"priority" = "priority",
|
||||
"labels__id" = "label_ids",
|
||||
"state__group" = "state__group",
|
||||
"assignees__id" = "assignee_ids",
|
||||
"cycle_id" = "cycle_id",
|
||||
"issue_module__module_id" = "module_ids",
|
||||
"target_date" = "target_date",
|
||||
"project_id" = "project_id",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
||||
export enum EServerGroupByToFilterOptions {
|
||||
"state_id" = "state",
|
||||
"priority" = "priority",
|
||||
"labels__id" = "labels",
|
||||
"state__group" = "state_group",
|
||||
"assignees__id" = "assignees",
|
||||
"cycle_id" = "cycle",
|
||||
"issue_module__module_id" = "module",
|
||||
"target_date" = "target_date",
|
||||
"project_id" = "project",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
||||
export enum EIssueServiceType {
|
||||
ISSUES = "issues",
|
||||
EPICS = "epics",
|
||||
}
|
||||
|
||||
export enum EIssueLayoutTypes {
|
||||
LIST = "list",
|
||||
KANBAN = "kanban",
|
||||
CALENDAR = "calendar",
|
||||
GANTT = "gantt_chart",
|
||||
SPREADSHEET = "spreadsheet",
|
||||
}
|
||||
|
||||
export enum EIssuesStoreType {
|
||||
GLOBAL = "GLOBAL",
|
||||
PROFILE = "PROFILE",
|
||||
TEAM = "TEAM",
|
||||
PROJECT = "PROJECT",
|
||||
CYCLE = "CYCLE",
|
||||
MODULE = "MODULE",
|
||||
TEAM_VIEW = "TEAM_VIEW",
|
||||
PROJECT_VIEW = "PROJECT_VIEW",
|
||||
ARCHIVED = "ARCHIVED",
|
||||
DRAFT = "DRAFT",
|
||||
DEFAULT = "DEFAULT",
|
||||
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
|
||||
EPIC = "EPIC",
|
||||
}
|
||||
|
||||
export enum EIssueFilterType {
|
||||
FILTERS = "filters",
|
||||
DISPLAY_FILTERS = "display_filters",
|
||||
DISPLAY_PROPERTIES = "display_properties",
|
||||
KANBAN_FILTERS = "kanban_filters",
|
||||
}
|
||||
|
||||
export enum EIssueCommentAccessSpecifier {
|
||||
EXTERNAL = "EXTERNAL",
|
||||
INTERNAL = "INTERNAL",
|
||||
}
|
||||
|
||||
export enum EIssueListRow {
|
||||
HEADER = "HEADER",
|
||||
ISSUE = "ISSUE",
|
||||
NO_ISSUES = "NO_ISSUES",
|
||||
QUICK_ADD = "QUICK_ADD",
|
||||
}
|
||||
|
||||
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
|
||||
} = {
|
||||
list: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
kanban: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
calendar: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
spreadsheet: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
gantt: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
};
|
||||
|
||||
export const ISSUE_PRIORITIES: {
|
||||
key: TIssuePriorities;
|
||||
title: string;
|
||||
}[] = [
|
||||
{ key: "urgent", title: "Urgent" },
|
||||
{ key: "high", title: "High" },
|
||||
{ key: "medium", title: "Medium" },
|
||||
{ key: "low", title: "Low" },
|
||||
{ key: "none", title: "None" },
|
||||
];
|
||||
|
||||
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
|
||||
{
|
||||
key: "urgent",
|
||||
title: "Urgent",
|
||||
className: "bg-red-500 border-red-500 text-white",
|
||||
icon: "error",
|
||||
},
|
||||
{
|
||||
key: "high",
|
||||
title: "High",
|
||||
className: "text-orange-500 border-custom-border-300",
|
||||
icon: "signal_cellular_alt",
|
||||
},
|
||||
{
|
||||
key: "medium",
|
||||
title: "Medium",
|
||||
className: "text-yellow-500 border-custom-border-300",
|
||||
icon: "signal_cellular_alt_2_bar",
|
||||
},
|
||||
{
|
||||
key: "low",
|
||||
title: "Low",
|
||||
className: "text-green-500 border-custom-border-300",
|
||||
icon: "signal_cellular_alt_1_bar",
|
||||
},
|
||||
{
|
||||
key: "none",
|
||||
title: "None",
|
||||
className: "text-gray-500 border-custom-border-300",
|
||||
icon: "block",
|
||||
},
|
||||
];
|
||||
|
||||
export const SITES_ISSUE_LAYOUTS: {
|
||||
key: TIssueLayout;
|
||||
title: string;
|
||||
icon: any;
|
||||
}[] = [
|
||||
{ key: "list", title: "List", icon: List },
|
||||
{ key: "kanban", title: "Kanban", icon: Kanban },
|
||||
// { key: "calendar", title: "Calendar", icon: Calendar },
|
||||
// { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
|
||||
// { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
|
||||
];
|
||||
@@ -0,0 +1,217 @@
|
||||
import {
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
IIssueDisplayProperties,
|
||||
} from "@plane/types";
|
||||
|
||||
export const ALL_ISSUES = "All Issues";
|
||||
|
||||
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
|
||||
|
||||
export type TIssueFilterPriorityObject = {
|
||||
key: TIssuePriorities;
|
||||
titleTranslationKey: string;
|
||||
className: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
export enum EIssueGroupByToServerOptions {
|
||||
"state" = "state_id",
|
||||
"priority" = "priority",
|
||||
"labels" = "labels__id",
|
||||
"state_detail.group" = "state__group",
|
||||
"assignees" = "assignees__id",
|
||||
"cycle" = "cycle_id",
|
||||
"module" = "issue_module__module_id",
|
||||
"target_date" = "target_date",
|
||||
"project" = "project_id",
|
||||
"created_by" = "created_by",
|
||||
"team_project" = "project_id",
|
||||
}
|
||||
|
||||
export enum EIssueGroupBYServerToProperty {
|
||||
"state_id" = "state_id",
|
||||
"priority" = "priority",
|
||||
"labels__id" = "label_ids",
|
||||
"state__group" = "state__group",
|
||||
"assignees__id" = "assignee_ids",
|
||||
"cycle_id" = "cycle_id",
|
||||
"issue_module__module_id" = "module_ids",
|
||||
"target_date" = "target_date",
|
||||
"project_id" = "project_id",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
||||
export enum EIssueServiceType {
|
||||
ISSUES = "issues",
|
||||
EPICS = "epics",
|
||||
}
|
||||
|
||||
export enum EIssuesStoreType {
|
||||
GLOBAL = "GLOBAL",
|
||||
PROFILE = "PROFILE",
|
||||
TEAM = "TEAM",
|
||||
PROJECT = "PROJECT",
|
||||
CYCLE = "CYCLE",
|
||||
MODULE = "MODULE",
|
||||
TEAM_VIEW = "TEAM_VIEW",
|
||||
PROJECT_VIEW = "PROJECT_VIEW",
|
||||
ARCHIVED = "ARCHIVED",
|
||||
DRAFT = "DRAFT",
|
||||
DEFAULT = "DEFAULT",
|
||||
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
|
||||
EPIC = "EPIC",
|
||||
}
|
||||
|
||||
export enum EIssueCommentAccessSpecifier {
|
||||
EXTERNAL = "EXTERNAL",
|
||||
INTERNAL = "INTERNAL",
|
||||
}
|
||||
|
||||
export enum EIssueListRow {
|
||||
HEADER = "HEADER",
|
||||
ISSUE = "ISSUE",
|
||||
NO_ISSUES = "NO_ISSUES",
|
||||
QUICK_ADD = "QUICK_ADD",
|
||||
}
|
||||
|
||||
export const ISSUE_PRIORITIES: {
|
||||
key: TIssuePriorities;
|
||||
title: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "urgent",
|
||||
title: "Urgent",
|
||||
},
|
||||
{
|
||||
key: "high",
|
||||
title: "High",
|
||||
},
|
||||
{
|
||||
key: "medium",
|
||||
title: "Medium",
|
||||
},
|
||||
{
|
||||
key: "low",
|
||||
title: "Low",
|
||||
},
|
||||
{
|
||||
key: "none",
|
||||
title: "None",
|
||||
},
|
||||
];
|
||||
|
||||
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
|
||||
"state",
|
||||
"priority",
|
||||
"assignees",
|
||||
"labels",
|
||||
"module",
|
||||
"cycle",
|
||||
];
|
||||
|
||||
export type TCreateModalStoreTypes =
|
||||
| EIssuesStoreType.TEAM
|
||||
| EIssuesStoreType.PROJECT
|
||||
| EIssuesStoreType.TEAM_VIEW
|
||||
| EIssuesStoreType.PROJECT_VIEW
|
||||
| EIssuesStoreType.PROFILE
|
||||
| EIssuesStoreType.CYCLE
|
||||
| EIssuesStoreType.MODULE
|
||||
| EIssuesStoreType.EPIC;
|
||||
|
||||
export const ISSUE_GROUP_BY_OPTIONS: {
|
||||
key: TIssueGroupByOptions;
|
||||
titleTranslationKey: string;
|
||||
}[] = [
|
||||
{ key: "state", titleTranslationKey: "common.states" },
|
||||
{ key: "state_detail.group", titleTranslationKey: "common.state_groups" },
|
||||
{ key: "priority", titleTranslationKey: "common.priority" },
|
||||
{ key: "team_project", titleTranslationKey: "common.team_project" }, // required this on team issues
|
||||
{ key: "project", titleTranslationKey: "common.project" }, // required this on my issues
|
||||
{ key: "cycle", titleTranslationKey: "common.cycle" }, // required this on my issues
|
||||
{ key: "module", titleTranslationKey: "common.module" }, // required this on my issues
|
||||
{ key: "labels", titleTranslationKey: "common.labels" },
|
||||
{ key: "assignees", titleTranslationKey: "common.assignees" },
|
||||
{ key: "created_by", titleTranslationKey: "common.created_by" },
|
||||
{ key: null, titleTranslationKey: "common.none" },
|
||||
];
|
||||
|
||||
export const ISSUE_ORDER_BY_OPTIONS: {
|
||||
key: TIssueOrderByOptions;
|
||||
titleTranslationKey: string;
|
||||
}[] = [
|
||||
{ key: "sort_order", titleTranslationKey: "common.order_by.manual" },
|
||||
{ key: "-created_at", titleTranslationKey: "common.order_by.last_created" },
|
||||
{ key: "-updated_at", titleTranslationKey: "common.order_by.last_updated" },
|
||||
{ key: "start_date", titleTranslationKey: "common.order_by.start_date" },
|
||||
{ key: "target_date", titleTranslationKey: "common.order_by.due_date" },
|
||||
{ key: "-priority", titleTranslationKey: "common.priority" },
|
||||
];
|
||||
|
||||
export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] =
|
||||
[
|
||||
"assignee",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"labels",
|
||||
"key",
|
||||
"priority",
|
||||
"state",
|
||||
"sub_issue_count",
|
||||
"link",
|
||||
"attachment_count",
|
||||
"estimate",
|
||||
"created_on",
|
||||
"updated_on",
|
||||
"modules",
|
||||
"cycle",
|
||||
"issue_type",
|
||||
];
|
||||
|
||||
export const ISSUE_DISPLAY_PROPERTIES: {
|
||||
key: keyof IIssueDisplayProperties;
|
||||
titleTranslationKey: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "key",
|
||||
titleTranslationKey: "issue.display.properties.id",
|
||||
},
|
||||
{
|
||||
key: "issue_type",
|
||||
titleTranslationKey: "issue.display.properties.issue_type",
|
||||
},
|
||||
{
|
||||
key: "assignee",
|
||||
titleTranslationKey: "common.assignee",
|
||||
},
|
||||
{
|
||||
key: "start_date",
|
||||
titleTranslationKey: "common.order_by.start_date",
|
||||
},
|
||||
{
|
||||
key: "due_date",
|
||||
titleTranslationKey: "common.order_by.due_date",
|
||||
},
|
||||
{ key: "labels", titleTranslationKey: "common.labels" },
|
||||
{
|
||||
key: "priority",
|
||||
titleTranslationKey: "common.priority",
|
||||
},
|
||||
{ key: "state", titleTranslationKey: "common.state" },
|
||||
{
|
||||
key: "sub_issue_count",
|
||||
titleTranslationKey: "issue.display.properties.sub_issue_count",
|
||||
},
|
||||
{
|
||||
key: "attachment_count",
|
||||
titleTranslationKey: "issue.display.properties.attachment_count",
|
||||
},
|
||||
{ key: "link", titleTranslationKey: "common.link" },
|
||||
{
|
||||
key: "estimate",
|
||||
titleTranslationKey: "common.estimate",
|
||||
},
|
||||
{ key: "modules", titleTranslationKey: "common.module" },
|
||||
{ key: "cycle", titleTranslationKey: "common.cycle" },
|
||||
];
|
||||
@@ -0,0 +1,530 @@
|
||||
import {
|
||||
ILayoutDisplayFiltersOptions,
|
||||
TIssueActivityComment,
|
||||
} from "@plane/types";
|
||||
import {
|
||||
TIssueFilterPriorityObject,
|
||||
ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
EIssuesStoreType,
|
||||
} from "./common";
|
||||
|
||||
import { TIssueLayout } from "./layout";
|
||||
|
||||
export type TIssueFilterKeys = "priority" | "state" | "labels";
|
||||
|
||||
export enum EServerGroupByToFilterOptions {
|
||||
"state_id" = "state",
|
||||
"priority" = "priority",
|
||||
"labels__id" = "labels",
|
||||
"state__group" = "state_group",
|
||||
"assignees__id" = "assignees",
|
||||
"cycle_id" = "cycle",
|
||||
"issue_module__module_id" = "module",
|
||||
"target_date" = "target_date",
|
||||
"project_id" = "project",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
||||
export enum EIssueFilterType {
|
||||
FILTERS = "filters",
|
||||
DISPLAY_FILTERS = "display_filters",
|
||||
DISPLAY_PROPERTIES = "display_properties",
|
||||
KANBAN_FILTERS = "kanban_filters",
|
||||
}
|
||||
|
||||
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
|
||||
} = {
|
||||
list: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
kanban: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
calendar: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
spreadsheet: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
gantt: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
},
|
||||
};
|
||||
|
||||
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
|
||||
{
|
||||
key: "urgent",
|
||||
titleTranslationKey: "issue.priority.urgent",
|
||||
className: "bg-red-500 border-red-500 text-white",
|
||||
icon: "error",
|
||||
},
|
||||
{
|
||||
key: "high",
|
||||
titleTranslationKey: "issue.priority.high",
|
||||
className: "text-orange-500 border-custom-border-300",
|
||||
icon: "signal_cellular_alt",
|
||||
},
|
||||
{
|
||||
key: "medium",
|
||||
titleTranslationKey: "issue.priority.medium",
|
||||
className: "text-yellow-500 border-custom-border-300",
|
||||
icon: "signal_cellular_alt_2_bar",
|
||||
},
|
||||
{
|
||||
key: "low",
|
||||
titleTranslationKey: "issue.priority.low",
|
||||
className: "text-green-500 border-custom-border-300",
|
||||
icon: "signal_cellular_alt_1_bar",
|
||||
},
|
||||
{
|
||||
key: "none",
|
||||
titleTranslationKey: "common.none",
|
||||
className: "text-gray-500 border-custom-border-300",
|
||||
icon: "block",
|
||||
},
|
||||
];
|
||||
|
||||
export type TFiltersByLayout = {
|
||||
[layoutType: string]: ILayoutDisplayFiltersOptions;
|
||||
};
|
||||
|
||||
export type TIssueFiltersToDisplayByPageType = {
|
||||
[pageType: string]: TFiltersByLayout;
|
||||
};
|
||||
|
||||
export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
||||
profile_issues: {
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: ["state_detail.group", "priority", "project", "labels", null],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["show_empty_groups", "sub_issue"],
|
||||
},
|
||||
},
|
||||
kanban: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: ["state_detail.group", "priority", "project", "labels"],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["show_empty_groups"],
|
||||
},
|
||||
},
|
||||
},
|
||||
archived_issues: {
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"issue_type",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: [
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"state_detail.group",
|
||||
"priority",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
null,
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["show_empty_groups"],
|
||||
},
|
||||
},
|
||||
},
|
||||
draft_issues: {
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"issue_type",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: [
|
||||
"state_detail.group",
|
||||
"cycle",
|
||||
"module",
|
||||
"priority",
|
||||
"project",
|
||||
"labels",
|
||||
null,
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["show_empty_groups"],
|
||||
},
|
||||
},
|
||||
kanban: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"issue_type",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: [
|
||||
"state_detail.group",
|
||||
"cycle",
|
||||
"module",
|
||||
"priority",
|
||||
"project",
|
||||
"labels",
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["show_empty_groups"],
|
||||
},
|
||||
},
|
||||
},
|
||||
my_issues: {
|
||||
spreadsheet: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
"subscriber",
|
||||
"project",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
order_by: [],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["sub_issue"],
|
||||
},
|
||||
},
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
"subscriber",
|
||||
"project",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: false,
|
||||
values: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
issues: {
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"issue_type",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: [
|
||||
"state",
|
||||
"priority",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
null,
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["show_empty_groups", "sub_issue"],
|
||||
},
|
||||
},
|
||||
kanban: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"issue_type",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: [
|
||||
"state",
|
||||
"priority",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
],
|
||||
sub_group_by: [
|
||||
"state",
|
||||
"priority",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
null,
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
"target_date",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["show_empty_groups", "sub_issue"],
|
||||
},
|
||||
},
|
||||
calendar: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"issue_type",
|
||||
],
|
||||
display_properties: ["key", "issue_type"],
|
||||
display_filters: {
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["sub_issue"],
|
||||
},
|
||||
},
|
||||
spreadsheet: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"issue_type",
|
||||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["sub_issue"],
|
||||
},
|
||||
},
|
||||
gantt_chart: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"issue_type",
|
||||
],
|
||||
display_properties: ["key", "issue_type"],
|
||||
display_filters: {
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["sub_issue"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ISSUE_STORE_TO_FILTERS_MAP: Partial<
|
||||
Record<EIssuesStoreType, TFiltersByLayout>
|
||||
> = {
|
||||
[EIssuesStoreType.PROJECT]: ISSUE_DISPLAY_FILTERS_BY_PAGE.issues,
|
||||
};
|
||||
|
||||
export enum EActivityFilterType {
|
||||
ACTIVITY = "ACTIVITY",
|
||||
COMMENT = "COMMENT",
|
||||
}
|
||||
|
||||
export type TActivityFilters = EActivityFilterType;
|
||||
|
||||
export const ACTIVITY_FILTER_TYPE_OPTIONS: Record<
|
||||
TActivityFilters,
|
||||
{ labelTranslationKey: string }
|
||||
> = {
|
||||
[EActivityFilterType.ACTIVITY]: {
|
||||
labelTranslationKey: "common.updates",
|
||||
},
|
||||
[EActivityFilterType.COMMENT]: {
|
||||
labelTranslationKey: "common.comments",
|
||||
},
|
||||
};
|
||||
|
||||
export type TActivityFilterOption = {
|
||||
key: TActivityFilters;
|
||||
labelTranslationKey: string;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const defaultActivityFilters: TActivityFilters[] = [
|
||||
EActivityFilterType.ACTIVITY,
|
||||
EActivityFilterType.COMMENT,
|
||||
];
|
||||
|
||||
export const filterActivityOnSelectedFilters = (
|
||||
activity: TIssueActivityComment[],
|
||||
filters: TActivityFilters[]
|
||||
): TIssueActivityComment[] =>
|
||||
activity.filter((activity) =>
|
||||
filters.includes(activity.activity_type as TActivityFilters)
|
||||
);
|
||||
|
||||
export const ENABLE_ISSUE_DEPENDENCIES = false;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./common";
|
||||
export * from "./filter";
|
||||
export * from "./layout";
|
||||
@@ -0,0 +1,76 @@
|
||||
export type TIssueLayout =
|
||||
| "list"
|
||||
| "kanban"
|
||||
| "calendar"
|
||||
| "spreadsheet"
|
||||
| "gantt";
|
||||
|
||||
export enum EIssueLayoutTypes {
|
||||
LIST = "list",
|
||||
KANBAN = "kanban",
|
||||
CALENDAR = "calendar",
|
||||
GANTT = "gantt_chart",
|
||||
SPREADSHEET = "spreadsheet",
|
||||
}
|
||||
|
||||
export type TIssueLayoutMap = Record<
|
||||
EIssueLayoutTypes,
|
||||
{
|
||||
key: EIssueLayoutTypes;
|
||||
i18n_title: string;
|
||||
i18n_label: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const SITES_ISSUE_LAYOUTS: {
|
||||
key: TIssueLayout;
|
||||
titleTranslationKey: string;
|
||||
icon: any;
|
||||
}[] = [
|
||||
{
|
||||
key: "list",
|
||||
icon: "List",
|
||||
titleTranslationKey: "issue.layouts.list",
|
||||
},
|
||||
{
|
||||
key: "kanban",
|
||||
icon: "Kanban",
|
||||
titleTranslationKey: "issue.layouts.kanban",
|
||||
},
|
||||
// { key: "calendar", title: "Calendar", icon: Calendar },
|
||||
// { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
|
||||
// { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
|
||||
];
|
||||
|
||||
export const ISSUE_LAYOUT_MAP: TIssueLayoutMap = {
|
||||
[EIssueLayoutTypes.LIST]: {
|
||||
key: EIssueLayoutTypes.LIST,
|
||||
i18n_title: "issue.layouts.title.list",
|
||||
i18n_label: "issue.layouts.list",
|
||||
},
|
||||
[EIssueLayoutTypes.KANBAN]: {
|
||||
key: EIssueLayoutTypes.KANBAN,
|
||||
i18n_title: "issue.layouts.title.kanban",
|
||||
i18n_label: "issue.layouts.kanban",
|
||||
},
|
||||
[EIssueLayoutTypes.CALENDAR]: {
|
||||
key: EIssueLayoutTypes.CALENDAR,
|
||||
i18n_title: "issue.layouts.title.calendar",
|
||||
i18n_label: "issue.layouts.calendar",
|
||||
},
|
||||
[EIssueLayoutTypes.SPREADSHEET]: {
|
||||
key: EIssueLayoutTypes.SPREADSHEET,
|
||||
i18n_title: "issue.layouts.title.spreadsheet",
|
||||
i18n_label: "issue.layouts.spreadsheet",
|
||||
},
|
||||
[EIssueLayoutTypes.GANTT]: {
|
||||
key: EIssueLayoutTypes.GANTT,
|
||||
i18n_title: "issue.layouts.title.gantt",
|
||||
i18n_label: "issue.layouts.gantt",
|
||||
},
|
||||
};
|
||||
|
||||
export const ISSUE_LAYOUTS: {
|
||||
key: EIssueLayoutTypes;
|
||||
i18n_title: string;
|
||||
}[] = Object.values(ISSUE_LAYOUT_MAP);
|
||||
@@ -1,48 +1,38 @@
|
||||
import React from "react";
|
||||
// icons
|
||||
import { Activity, Bell, CircleUser, KeyRound, LucideProps, Settings2 } from "lucide-react";
|
||||
|
||||
export const PROFILE_ACTION_LINKS: {
|
||||
key: string;
|
||||
label: string;
|
||||
i18n_label: string;
|
||||
href: string;
|
||||
highlight: (pathname: string) => boolean;
|
||||
Icon: React.FC<LucideProps>;
|
||||
}[] = [
|
||||
{
|
||||
key: "profile",
|
||||
label: "Profile",
|
||||
i18n_label: "profile.actions.profile",
|
||||
href: `/profile`,
|
||||
highlight: (pathname: string) => pathname === "/profile/",
|
||||
Icon: CircleUser,
|
||||
},
|
||||
{
|
||||
key: "security",
|
||||
label: "Security",
|
||||
i18n_label: "profile.actions.security",
|
||||
href: `/profile/security`,
|
||||
highlight: (pathname: string) => pathname === "/profile/security/",
|
||||
Icon: KeyRound,
|
||||
},
|
||||
{
|
||||
key: "activity",
|
||||
label: "Activity",
|
||||
i18n_label: "profile.actions.activity",
|
||||
href: `/profile/activity`,
|
||||
highlight: (pathname: string) => pathname === "/profile/activity/",
|
||||
Icon: Activity,
|
||||
},
|
||||
{
|
||||
key: "appearance",
|
||||
label: "Appearance",
|
||||
i18n_label: "profile.actions.appearance",
|
||||
href: `/profile/appearance`,
|
||||
highlight: (pathname: string) => pathname.includes("/profile/appearance"),
|
||||
Icon: Settings2,
|
||||
},
|
||||
{
|
||||
key: "notifications",
|
||||
label: "Notifications",
|
||||
i18n_label: "profile.actions.notifications",
|
||||
href: `/profile/notifications`,
|
||||
highlight: (pathname: string) => pathname === "/profile/notifications/",
|
||||
Icon: Bell,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -50,7 +40,7 @@ export const PROFILE_VIEWER_TAB = [
|
||||
{
|
||||
key: "summary",
|
||||
route: "",
|
||||
label: "Summary",
|
||||
i18n_label: "profile.tabs.summary",
|
||||
selected: "/",
|
||||
},
|
||||
];
|
||||
@@ -59,24 +49,25 @@ export const PROFILE_ADMINS_TAB = [
|
||||
{
|
||||
key: "assigned",
|
||||
route: "assigned",
|
||||
label: "Assigned",
|
||||
i18n_label: "profile.tabs.assigned",
|
||||
selected: "/assigned/",
|
||||
},
|
||||
{
|
||||
key: "created",
|
||||
route: "created",
|
||||
label: "Created",
|
||||
i18n_label: "profile.tabs.created",
|
||||
selected: "/created/",
|
||||
},
|
||||
{
|
||||
key: "subscribed",
|
||||
route: "subscribed",
|
||||
label: "Subscribed",
|
||||
i18n_label: "profile.tabs.subscribed",
|
||||
selected: "/subscribed/",
|
||||
},
|
||||
{
|
||||
key: "activity",
|
||||
route: "activity",
|
||||
label: "Activity",
|
||||
i18n_label: "profile.tabs.activity",
|
||||
selected: "/activity/",
|
||||
},
|
||||
];
|
||||
@@ -1,41 +1,59 @@
|
||||
// icons
|
||||
import { Globe2, Lock, LucideIcon } from "lucide-react";
|
||||
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
|
||||
import {
|
||||
TProjectAppliedDisplayFilterKeys,
|
||||
TProjectOrderByOptions,
|
||||
} from "@plane/types";
|
||||
|
||||
export const NETWORK_CHOICES: {
|
||||
key: 0 | 2;
|
||||
label: string;
|
||||
i18n_label: string;
|
||||
description: string;
|
||||
icon: LucideIcon;
|
||||
}[] = [
|
||||
{
|
||||
key: 0,
|
||||
label: "Private",
|
||||
description: "Accessible only by invite",
|
||||
i18n_label: "workspace_projects.network.private.title",
|
||||
description: "workspace_projects.network.private.description", //"Accessible only by invite",
|
||||
icon: Lock,
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
label: "Public",
|
||||
description: "Anyone in the workspace except Guests can join",
|
||||
i18n_label: "workspace_projects.network.public.title",
|
||||
description: "workspace_projects.network.public.description", //"Anyone in the workspace except Guests can join",
|
||||
icon: Globe2,
|
||||
},
|
||||
];
|
||||
|
||||
export const GROUP_CHOICES = {
|
||||
backlog: "Backlog",
|
||||
unstarted: "Unstarted",
|
||||
started: "Started",
|
||||
completed: "Completed",
|
||||
cancelled: "Cancelled",
|
||||
backlog: {
|
||||
key: "backlog",
|
||||
i18n_label: "workspace_projects.state.backlog",
|
||||
},
|
||||
unstarted: {
|
||||
key: "unstarted",
|
||||
i18n_label: "workspace_projects.state.unstarted",
|
||||
},
|
||||
started: {
|
||||
key: "started",
|
||||
i18n_label: "workspace_projects.state.started",
|
||||
},
|
||||
completed: {
|
||||
key: "completed",
|
||||
i18n_label: "workspace_projects.state.completed",
|
||||
},
|
||||
cancelled: {
|
||||
key: "cancelled",
|
||||
i18n_label: "workspace_projects.state.cancelled",
|
||||
},
|
||||
};
|
||||
|
||||
export const PROJECT_AUTOMATION_MONTHS = [
|
||||
{ label: "1 month", value: 1 },
|
||||
{ label: "3 months", value: 3 },
|
||||
{ label: "6 months", value: 6 },
|
||||
{ label: "9 months", value: 9 },
|
||||
{ label: "12 months", value: 12 },
|
||||
{ i18n_label: "common.months_count", value: 1 },
|
||||
{ i18n_label: "common.months_count", value: 3 },
|
||||
{ i18n_label: "common.months_count", value: 6 },
|
||||
{ i18n_label: "common.months_count", value: 9 },
|
||||
{ i18n_label: "common.months_count", value: 12 },
|
||||
];
|
||||
|
||||
export const PROJECT_UNSPLASH_COVERS = [
|
||||
@@ -59,55 +77,55 @@ export const PROJECT_UNSPLASH_COVERS = [
|
||||
|
||||
export const PROJECT_ORDER_BY_OPTIONS: {
|
||||
key: TProjectOrderByOptions;
|
||||
label: string;
|
||||
i18n_label: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "sort_order",
|
||||
label: "Manual",
|
||||
i18n_label: "workspace_projects.sort.manual",
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
i18n_label: "workspace_projects.sort.name",
|
||||
},
|
||||
{
|
||||
key: "created_at",
|
||||
label: "Created date",
|
||||
i18n_label: "workspace_projects.sort.created_at",
|
||||
},
|
||||
{
|
||||
key: "members_length",
|
||||
label: "Number of members",
|
||||
i18n_label: "workspace_projects.sort.members_length",
|
||||
},
|
||||
];
|
||||
|
||||
export const PROJECT_DISPLAY_FILTER_OPTIONS: {
|
||||
key: TProjectAppliedDisplayFilterKeys;
|
||||
label: string;
|
||||
i18n_label: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "my_projects",
|
||||
label: "My projects",
|
||||
i18n_label: "workspace_projects.scope.my_projects",
|
||||
},
|
||||
{
|
||||
key: "archived_projects",
|
||||
label: "Archived",
|
||||
i18n_label: "workspace_projects.scope.archived_projects",
|
||||
},
|
||||
];
|
||||
|
||||
export const PROJECT_ERROR_MESSAGES = {
|
||||
permissionError: {
|
||||
title: "You don't have permission to perform this action.",
|
||||
message: undefined,
|
||||
i18n_title: "workspace_projects.error.permission",
|
||||
i18n_message: undefined,
|
||||
},
|
||||
cycleDeleteError: {
|
||||
title: "Error",
|
||||
message: "Failed to delete cycle",
|
||||
i18n_title: "error",
|
||||
i18n_message: "workspace_projects.error.cycle_delete",
|
||||
},
|
||||
moduleDeleteError: {
|
||||
title: "Error",
|
||||
message: "Failed to delete module",
|
||||
i18n_title: "error",
|
||||
i18n_message: "workspace_projects.error.module_delete",
|
||||
},
|
||||
issueDeleteError: {
|
||||
title: "Error",
|
||||
message: "Failed to delete issue",
|
||||
i18n_title: "error",
|
||||
i18n_message: "workspace_projects.error.issue_delete",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const STICKIES_PER_PAGE = 30;
|
||||
@@ -54,7 +54,14 @@ export const PROJECT_CREATE_TAB_INDICES = [
|
||||
"logo_props",
|
||||
];
|
||||
|
||||
export const PROJECT_CYCLE_TAB_INDICES = ["name", "description", "date_range", "cancel", "submit", "project_id"];
|
||||
export const PROJECT_CYCLE_TAB_INDICES = [
|
||||
"name",
|
||||
"description",
|
||||
"date_range",
|
||||
"cancel",
|
||||
"submit",
|
||||
"project_id",
|
||||
];
|
||||
|
||||
export const PROJECT_MODULE_TAB_INDICES = [
|
||||
"name",
|
||||
@@ -67,9 +74,21 @@ export const PROJECT_MODULE_TAB_INDICES = [
|
||||
"submit",
|
||||
];
|
||||
|
||||
export const PROJECT_VIEW_TAB_INDICES = ["name", "description", "filters", "cancel", "submit"];
|
||||
export const PROJECT_VIEW_TAB_INDICES = [
|
||||
"name",
|
||||
"description",
|
||||
"filters",
|
||||
"cancel",
|
||||
"submit",
|
||||
];
|
||||
|
||||
export const PROJECT_PAGE_TAB_INDICES = ["name", "public", "private", "cancel", "submit"];
|
||||
export const PROJECT_PAGE_TAB_INDICES = [
|
||||
"name",
|
||||
"public",
|
||||
"private",
|
||||
"cancel",
|
||||
"submit",
|
||||
];
|
||||
|
||||
export enum ETabIndices {
|
||||
ISSUE_FORM = "issue-form",
|
||||
@@ -0,0 +1,23 @@
|
||||
export enum EViewAccess {
|
||||
PRIVATE,
|
||||
PUBLIC,
|
||||
}
|
||||
|
||||
export const VIEW_ACCESS_SPECIFIERS: {
|
||||
key: EViewAccess;
|
||||
i18n_label: string;
|
||||
}[] = [
|
||||
{ key: EViewAccess.PUBLIC, i18n_label: "project_view.access.public" },
|
||||
{ key: EViewAccess.PRIVATE, i18n_label: "project_view.access.private" },
|
||||
];
|
||||
|
||||
export const VIEW_SORTING_KEY_OPTIONS = [
|
||||
{ key: "name", i18n_label: "project_view.sort_by.name" },
|
||||
{ key: "created_at", i18n_label: "project_view.sort_by.created_at" },
|
||||
{ key: "updated_at", i18n_label: "project_view.sort_by.updated_at" },
|
||||
];
|
||||
|
||||
export const VIEW_SORT_BY_OPTIONS = [
|
||||
{ key: "asc", i18n_label: "common.order_by.asc" },
|
||||
{ key: "desc", i18n_label: "common.order_by.desc" },
|
||||
];
|
||||
@@ -1,3 +1,6 @@
|
||||
import { TStaticViewTypes } from "@plane/types";
|
||||
import { EUserWorkspaceRoles } from "./user";
|
||||
|
||||
export const ORGANIZATION_SIZE = [
|
||||
"Just myself", // TODO: translate
|
||||
"2-10",
|
||||
@@ -74,3 +77,182 @@ export const RESTRICTED_URLS = [
|
||||
"instances",
|
||||
"instance",
|
||||
];
|
||||
|
||||
export const WORKSPACE_SETTINGS = {
|
||||
general: {
|
||||
key: "general",
|
||||
i18n_label: "workspace_settings.settings.general.title",
|
||||
href: `/settings`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) =>
|
||||
pathname === `${baseUrl}/settings/`,
|
||||
},
|
||||
members: {
|
||||
key: "members",
|
||||
i18n_label: "workspace_settings.settings.members.title",
|
||||
href: `/settings/members`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) =>
|
||||
pathname === `${baseUrl}/settings/members/`,
|
||||
},
|
||||
"billing-and-plans": {
|
||||
key: "billing-and-plans",
|
||||
i18n_label: "workspace_settings.settings.billing_and_plans.title",
|
||||
href: `/settings/billing`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) =>
|
||||
pathname === `${baseUrl}/settings/billing/`,
|
||||
},
|
||||
export: {
|
||||
key: "export",
|
||||
i18n_label: "workspace_settings.settings.exports.title",
|
||||
href: `/settings/exports`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) =>
|
||||
pathname === `${baseUrl}/settings/exports/`,
|
||||
},
|
||||
webhooks: {
|
||||
key: "webhooks",
|
||||
i18n_label: "workspace_settings.settings.webhooks.title",
|
||||
href: `/settings/webhooks`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) =>
|
||||
pathname === `${baseUrl}/settings/webhooks/`,
|
||||
},
|
||||
"api-tokens": {
|
||||
key: "api-tokens",
|
||||
i18n_label: "workspace_settings.settings.webhooks.title",
|
||||
href: `/settings/api-tokens`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) =>
|
||||
pathname === `${baseUrl}/settings/api-tokens/`,
|
||||
},
|
||||
};
|
||||
|
||||
export const WORKSPACE_SETTINGS_LINKS: {
|
||||
key: string;
|
||||
i18n_label: string;
|
||||
href: string;
|
||||
access: EUserWorkspaceRoles[];
|
||||
highlight: (pathname: string, baseUrl: string) => boolean;
|
||||
}[] = [
|
||||
WORKSPACE_SETTINGS["general"],
|
||||
WORKSPACE_SETTINGS["members"],
|
||||
WORKSPACE_SETTINGS["billing-and-plans"],
|
||||
WORKSPACE_SETTINGS["export"],
|
||||
WORKSPACE_SETTINGS["webhooks"],
|
||||
WORKSPACE_SETTINGS["api-tokens"],
|
||||
];
|
||||
|
||||
export const ROLE = {
|
||||
[EUserWorkspaceRoles.GUEST]: "Guest",
|
||||
[EUserWorkspaceRoles.MEMBER]: "Member",
|
||||
[EUserWorkspaceRoles.ADMIN]: "Admin",
|
||||
};
|
||||
|
||||
export const ROLE_DETAILS = {
|
||||
[EUserWorkspaceRoles.GUEST]: {
|
||||
i18n_title: "role_details.guest.title",
|
||||
i18n_description: "role_details.guest.description",
|
||||
},
|
||||
[EUserWorkspaceRoles.MEMBER]: {
|
||||
i18n_title: "role_details.member.title",
|
||||
i18n_description: "role_details.member.description",
|
||||
},
|
||||
[EUserWorkspaceRoles.ADMIN]: {
|
||||
i18n_title: "role_details.admin.title",
|
||||
i18n_description: "role_details.admin.description",
|
||||
},
|
||||
};
|
||||
|
||||
export const USER_ROLES = [
|
||||
{
|
||||
value: "Product / Project Manager",
|
||||
i18n_label: "user_roles.product_or_project_manager",
|
||||
},
|
||||
{
|
||||
value: "Development / Engineering",
|
||||
i18n_label: "user_roles.development_or_engineering",
|
||||
},
|
||||
{
|
||||
value: "Founder / Executive",
|
||||
i18n_label: "user_roles.founder_or_executive",
|
||||
},
|
||||
{
|
||||
value: "Freelancer / Consultant",
|
||||
i18n_label: "user_roles.freelancer_or_consultant",
|
||||
},
|
||||
{ value: "Marketing / Growth", i18n_label: "user_roles.marketing_or_growth" },
|
||||
{
|
||||
value: "Sales / Business Development",
|
||||
i18n_label: "user_roles.sales_or_business_development",
|
||||
},
|
||||
{
|
||||
value: "Support / Operations",
|
||||
i18n_label: "user_roles.support_or_operations",
|
||||
},
|
||||
{
|
||||
value: "Student / Professor",
|
||||
i18n_label: "user_roles.student_or_professor",
|
||||
},
|
||||
{ value: "Human Resources", i18n_label: "user_roles.human_resources" },
|
||||
{ value: "Other", i18n_label: "user_roles.other" },
|
||||
];
|
||||
|
||||
export const IMPORTERS_LIST = [
|
||||
{
|
||||
provider: "github",
|
||||
type: "import",
|
||||
i18n_title: "importer.github.title",
|
||||
i18n_description: "importer.github.description",
|
||||
},
|
||||
{
|
||||
provider: "jira",
|
||||
type: "import",
|
||||
i18n_title: "importer.jira.title",
|
||||
i18n_description: "importer.jira.description",
|
||||
},
|
||||
];
|
||||
|
||||
export const EXPORTERS_LIST = [
|
||||
{
|
||||
provider: "csv",
|
||||
type: "export",
|
||||
i18n_title: "exporter.csv.title",
|
||||
i18n_description: "exporter.csv.description",
|
||||
},
|
||||
{
|
||||
provider: "xlsx",
|
||||
type: "export",
|
||||
i18n_title: "exporter.excel.title",
|
||||
i18n_description: "exporter.csv.description",
|
||||
},
|
||||
{
|
||||
provider: "json",
|
||||
type: "export",
|
||||
i18n_title: "exporter.json.title",
|
||||
i18n_description: "exporter.csv.description",
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_GLOBAL_VIEWS_LIST: {
|
||||
key: TStaticViewTypes;
|
||||
i18n_label: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "all-issues",
|
||||
i18n_label: "default_global_view.all_issues",
|
||||
},
|
||||
{
|
||||
key: "assigned",
|
||||
i18n_label: "default_global_view.assigned",
|
||||
},
|
||||
{
|
||||
key: "created",
|
||||
i18n_label: "default_global_view.created",
|
||||
},
|
||||
{
|
||||
key: "subscribed",
|
||||
i18n_label: "default_global_view.subscribed",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -12,14 +12,12 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs"
|
||||
"import": "./dist/index.mjs"
|
||||
},
|
||||
"./lib": {
|
||||
"require": "./dist/lib.js",
|
||||
"types": "./dist/lib.d.mts",
|
||||
"import": "./dist/lib.mjs",
|
||||
"module": "./dist/lib.mjs"
|
||||
"import": "./dist/lib.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -36,7 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.4",
|
||||
"@hocuspocus/provider": "^2.13.5",
|
||||
"@hocuspocus/provider": "^2.15.0",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/utils": "*",
|
||||
@@ -67,12 +65,12 @@
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
"prosemirror-utils": "^1.2.2",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8.9",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"uuid": "^10.0.0",
|
||||
"y-indexeddb": "^9.0.12",
|
||||
"y-prosemirror": "^1.2.5",
|
||||
"y-prosemirror": "^1.2.15",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.15"
|
||||
"yjs": "^13.6.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { Extensions } from "@tiptap/core";
|
||||
import { AnyExtension } from "@tiptap/core";
|
||||
import { SlashCommands } from "@/extensions";
|
||||
// plane editor types
|
||||
import { TIssueEmbedConfig } from "@/plane-editor/types";
|
||||
@@ -13,15 +14,24 @@ type Props = {
|
||||
userDetails: TUserDetails;
|
||||
};
|
||||
|
||||
type ExtensionConfig = {
|
||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||
getExtension: (props: Props) => AnyExtension;
|
||||
};
|
||||
|
||||
const extensionRegistry: ExtensionConfig[] = [
|
||||
{
|
||||
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
|
||||
getExtension: () => SlashCommands({}),
|
||||
},
|
||||
];
|
||||
|
||||
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
|
||||
const { disabledExtensions } = _props;
|
||||
const extensions: Extensions = disabledExtensions?.includes("slash-commands")
|
||||
? []
|
||||
: [
|
||||
SlashCommands({
|
||||
disabledExtensions,
|
||||
}),
|
||||
];
|
||||
|
||||
return extensions;
|
||||
const { disabledExtensions = [] } = _props;
|
||||
|
||||
const documentExtensions = extensionRegistry
|
||||
.filter((config) => config.isEnabled(disabledExtensions))
|
||||
.map((config) => config.getExtension(_props));
|
||||
|
||||
return documentExtensions;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TSlashCommandAdditionalOption } from "@/extensions";
|
||||
import { TExtensions } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
disabledExtensions?: TExtensions[];
|
||||
};
|
||||
|
||||
export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => {
|
||||
|
||||
@@ -202,8 +202,7 @@ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
|
||||
key: "image",
|
||||
name: "Image",
|
||||
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
|
||||
command: ({ savedSelection }) =>
|
||||
insertImage({ editor, event: "insert", pos: savedSelection?.from ?? editor.state.selection.from }),
|
||||
command: () => insertImage({ editor, event: "insert", pos: editor.state.selection.from }),
|
||||
icon: ImageIcon,
|
||||
});
|
||||
|
||||
|
||||
@@ -39,11 +39,11 @@ import {
|
||||
setText,
|
||||
} from "@/helpers/editor-commands";
|
||||
// types
|
||||
import { CommandProps, ISlashCommandItem, TExtensions, TSlashCommandSectionKeys } from "@/types";
|
||||
import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types";
|
||||
// plane editor extensions
|
||||
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
|
||||
// local types
|
||||
import { TSlashCommandAdditionalOption } from "./root";
|
||||
import { TExtensionProps } from "./root";
|
||||
|
||||
export type TSlashCommandSection = {
|
||||
key: TSlashCommandSectionKeys;
|
||||
@@ -51,13 +51,8 @@ export type TSlashCommandSection = {
|
||||
items: ISlashCommandItem[];
|
||||
};
|
||||
|
||||
type TArgs = {
|
||||
additionalOptions?: TSlashCommandAdditionalOption[];
|
||||
disabledExtensions: TExtensions[];
|
||||
};
|
||||
|
||||
export const getSlashCommandFilteredSections =
|
||||
(args: TArgs) =>
|
||||
(args: TExtensionProps) =>
|
||||
({ query }: { query: string }): TSlashCommandSection[] => {
|
||||
const { additionalOptions, disabledExtensions } = args;
|
||||
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
|
||||
|
||||
@@ -103,9 +103,9 @@ const renderItems = () => {
|
||||
};
|
||||
};
|
||||
|
||||
type TExtensionProps = {
|
||||
export type TExtensionProps = {
|
||||
additionalOptions?: TSlashCommandAdditionalOption[];
|
||||
disabledExtensions: TExtensions[];
|
||||
disabledExtensions?: TExtensions[];
|
||||
};
|
||||
|
||||
export const SlashCommands = (props: TExtensionProps) =>
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import { MutableRefObject } from "react";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
import { Editor } from "@tiptap/react";
|
||||
|
||||
export const insertContentAtSavedSelection = (
|
||||
editorRef: MutableRefObject<Editor | null>,
|
||||
content: string,
|
||||
savedSelection: Selection
|
||||
) => {
|
||||
if (!editorRef.current || editorRef.current.isDestroyed) {
|
||||
export const insertContentAtSavedSelection = (editor: Editor, content: string) => {
|
||||
if (!editor || editor.isDestroyed) {
|
||||
console.error("Editor reference is not available or has been destroyed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!savedSelection) {
|
||||
if (!editor.state.selection) {
|
||||
console.error("Saved selection is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
const docSize = editorRef.current.state.doc.content.size;
|
||||
const safePosition = Math.max(0, Math.min(savedSelection.anchor, docSize));
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const safePosition = Math.max(0, Math.min(editor.state.selection.anchor, docSize));
|
||||
|
||||
try {
|
||||
editorRef.current.chain().focus().insertContentAt(safePosition, content).run();
|
||||
editor.chain().focus().insertContentAt(safePosition, content).run();
|
||||
} catch (error) {
|
||||
console.error("An error occurred while inserting content at saved selection:", error);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { DOMSerializer } from "@tiptap/pm/model";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { useEditor as useTiptapEditor, Editor, Extensions } from "@tiptap/react";
|
||||
import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react";
|
||||
import { useImperativeHandle, MutableRefObject, useEffect } from "react";
|
||||
import * as Y from "yjs";
|
||||
// components
|
||||
import { EditorMenuItem, getEditorMenuItems } from "@/components/menus";
|
||||
import { getEditorMenuItems } from "@/components/menus";
|
||||
// extensions
|
||||
import { CoreEditorExtensions } from "@/extensions";
|
||||
// helpers
|
||||
@@ -71,14 +70,12 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
provider,
|
||||
autofocus = false,
|
||||
} = props;
|
||||
// states
|
||||
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
|
||||
// refs
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
const savedSelectionRef = useRef(savedSelection);
|
||||
|
||||
const editor = useTiptapEditor(
|
||||
{
|
||||
editable,
|
||||
immediatelyRender: false,
|
||||
shouldRerenderOnTransaction: false,
|
||||
autofocus,
|
||||
editorProps: {
|
||||
...CoreEditorProps({
|
||||
@@ -100,8 +97,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
],
|
||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
||||
onCreate: () => handleEditorReady?.(true),
|
||||
onTransaction: ({ editor }) => {
|
||||
setSavedSelection(editor.state.selection);
|
||||
onTransaction: () => {
|
||||
onTransaction?.();
|
||||
},
|
||||
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
|
||||
@@ -110,23 +106,17 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
[editable]
|
||||
);
|
||||
|
||||
// Update the ref whenever savedSelection changes
|
||||
useEffect(() => {
|
||||
savedSelectionRef.current = savedSelection;
|
||||
}, [savedSelection]);
|
||||
|
||||
// Effect for syncing SWR data
|
||||
useEffect(() => {
|
||||
// value is null when intentionally passed where syncing is not yet
|
||||
// supported and value is undefined when the data from swr is not populated
|
||||
if (value === null || value === undefined) return;
|
||||
if (value == null) return;
|
||||
if (editor && !editor.isDestroyed && !editor.storage.imageComponent.uploadInProgress) {
|
||||
try {
|
||||
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
|
||||
const currentSavedSelection = savedSelectionRef.current;
|
||||
if (currentSavedSelection) {
|
||||
if (editor.state.selection) {
|
||||
const docLength = editor.state.doc.content.size;
|
||||
const relativePosition = Math.min(currentSavedSelection.from, docLength - 1);
|
||||
const relativePosition = Math.min(editor.state.selection.from, docLength - 1);
|
||||
editor.commands.setTextSelection(relativePosition);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -138,46 +128,40 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
useImperativeHandle(
|
||||
forwardedRef,
|
||||
() => ({
|
||||
blur: () => editorRef.current?.commands.blur(),
|
||||
blur: () => editor.commands.blur(),
|
||||
scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) {
|
||||
const resolvedPos = pos ?? savedSelection?.from;
|
||||
if (!editorRef.current || !resolvedPos) return;
|
||||
scrollToNodeViaDOMCoordinates(editorRef.current, resolvedPos, behavior);
|
||||
const resolvedPos = pos ?? editor.state.selection.from;
|
||||
if (!editor || !resolvedPos) return;
|
||||
scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
|
||||
},
|
||||
getCurrentCursorPosition: () => savedSelection?.from,
|
||||
getCurrentCursorPosition: () => editor.state.selection.from,
|
||||
clearEditor: (emitUpdate = false) => {
|
||||
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
},
|
||||
setEditorValueAtCursorPosition: (content: string) => {
|
||||
if (savedSelection) {
|
||||
insertContentAtSavedSelection(editorRef, content, savedSelection);
|
||||
if (editor.state.selection) {
|
||||
insertContentAtSavedSelection(editor, content);
|
||||
}
|
||||
},
|
||||
executeMenuItemCommand: (props) => {
|
||||
const { itemKey } = props;
|
||||
const editorItems = getEditorMenuItems(editorRef.current);
|
||||
const editorItems = getEditorMenuItems(editor);
|
||||
|
||||
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
|
||||
|
||||
const item = getEditorMenuItem(itemKey);
|
||||
if (item) {
|
||||
if (item.key === "image") {
|
||||
(item as EditorMenuItem<"image">).command({
|
||||
savedSelection: savedSelectionRef.current,
|
||||
});
|
||||
} else {
|
||||
item.command(props);
|
||||
}
|
||||
item.command(props);
|
||||
} else {
|
||||
console.warn(`No command found for item: ${itemKey}`);
|
||||
}
|
||||
},
|
||||
isMenuItemActive: (props) => {
|
||||
const { itemKey } = props;
|
||||
const editorItems = getEditorMenuItems(editorRef.current);
|
||||
const editorItems = getEditorMenuItems(editor);
|
||||
|
||||
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
|
||||
const item = getEditorMenuItem(itemKey);
|
||||
@@ -187,20 +171,20 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
},
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
|
||||
// Subscribe to update event emitted from headers extension
|
||||
editorRef.current?.on("update", () => {
|
||||
callback(editorRef.current?.storage.headingList.headings);
|
||||
editor?.on("update", () => {
|
||||
callback(editor?.storage.headingList.headings);
|
||||
});
|
||||
// Return a function to unsubscribe to the continuous transactions of
|
||||
// the editor on unmounting the component that has subscribed to this
|
||||
// method
|
||||
return () => {
|
||||
editorRef.current?.off("update");
|
||||
editor?.off("update");
|
||||
};
|
||||
},
|
||||
getHeadings: () => editorRef?.current?.storage.headingList.headings,
|
||||
getHeadings: () => editor?.storage.headingList.headings,
|
||||
onStateChange: (callback: () => void) => {
|
||||
// Subscribe to editor state changes
|
||||
editorRef.current?.on("transaction", () => {
|
||||
editor?.on("transaction", () => {
|
||||
callback();
|
||||
});
|
||||
|
||||
@@ -208,17 +192,17 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
// the editor on unmounting the component that has subscribed to this
|
||||
// method
|
||||
return () => {
|
||||
editorRef.current?.off("transaction");
|
||||
editor?.off("transaction");
|
||||
};
|
||||
},
|
||||
getMarkDown: (): string => {
|
||||
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
|
||||
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
||||
return markdownOutput;
|
||||
},
|
||||
getDocument: () => {
|
||||
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
|
||||
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editorRef.current?.getJSON() ?? null;
|
||||
const documentHTML = editor?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editor.getJSON() ?? null;
|
||||
|
||||
return {
|
||||
binary: documentBinary,
|
||||
@@ -227,19 +211,19 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
};
|
||||
},
|
||||
scrollSummary: (marking: IMarking): void => {
|
||||
if (!editorRef.current) return;
|
||||
scrollSummary(editorRef.current, marking);
|
||||
if (!editor) return;
|
||||
scrollSummary(editor, marking);
|
||||
},
|
||||
isEditorReadyToDiscard: () => editorRef.current?.storage.imageComponent.uploadInProgress === false,
|
||||
isEditorReadyToDiscard: () => editor?.storage.imageComponent.uploadInProgress === false,
|
||||
setFocusAtPosition: (position: number) => {
|
||||
if (!editorRef.current || editorRef.current.isDestroyed) {
|
||||
if (!editor || editor.isDestroyed) {
|
||||
console.error("Editor reference is not available or has been destroyed.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const docSize = editorRef.current.state.doc.content.size;
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const safePosition = Math.max(0, Math.min(position, docSize));
|
||||
editorRef.current
|
||||
editor
|
||||
.chain()
|
||||
.insertContentAt(safePosition, [{ type: "paragraph" }])
|
||||
.focus()
|
||||
@@ -249,17 +233,17 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
}
|
||||
},
|
||||
getSelectedText: () => {
|
||||
if (!editorRef.current) return null;
|
||||
if (!editor) return null;
|
||||
|
||||
const { state } = editorRef.current;
|
||||
const { state } = editor;
|
||||
const { from, to, empty } = state.selection;
|
||||
|
||||
if (empty) return null;
|
||||
|
||||
const nodesArray: string[] = [];
|
||||
state.doc.nodesBetween(from, to, (node, _pos, parent) => {
|
||||
if (parent === state.doc && editorRef.current) {
|
||||
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema);
|
||||
if (parent === state.doc && editor) {
|
||||
const serializer = DOMSerializer.fromSchema(editor.schema);
|
||||
const dom = serializer.serializeNode(node);
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.appendChild(dom);
|
||||
@@ -270,28 +254,21 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
return selection;
|
||||
},
|
||||
insertText: (contentHTML, insertOnNextLine) => {
|
||||
if (!editorRef.current) return;
|
||||
// get selection
|
||||
const { from, to, empty } = editorRef.current.state.selection;
|
||||
if (!editor) return;
|
||||
const { from, to, empty } = editor.state.selection;
|
||||
if (empty) return;
|
||||
if (insertOnNextLine) {
|
||||
// move cursor to the end of the selection and insert a new line
|
||||
editorRef.current
|
||||
.chain()
|
||||
.focus()
|
||||
.setTextSelection(to)
|
||||
.insertContent("<br />")
|
||||
.insertContent(contentHTML)
|
||||
.run();
|
||||
editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
|
||||
} else {
|
||||
// replace selected text with the content provided
|
||||
editorRef.current.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
|
||||
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
|
||||
}
|
||||
},
|
||||
getDocumentInfo: () => ({
|
||||
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editorRef?.current?.state),
|
||||
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
|
||||
characters: editor?.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editor?.state),
|
||||
words: editor?.storage?.characterCount?.words?.() ?? 0,
|
||||
}),
|
||||
setProviderDocument: (value) => {
|
||||
const document = provider?.document;
|
||||
@@ -301,16 +278,12 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
|
||||
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
|
||||
}),
|
||||
[editorRef, savedSelection]
|
||||
[editor]
|
||||
);
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// the editorRef is used to access the editor instance from outside the hook
|
||||
// and should only be used after editor is initialized
|
||||
editorRef.current = editor;
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { useEditor as useCustomEditor, Editor, Extensions } from "@tiptap/react";
|
||||
import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react";
|
||||
import { useImperativeHandle, MutableRefObject, useEffect } from "react";
|
||||
import * as Y from "yjs";
|
||||
// extensions
|
||||
import { CoreReadOnlyEditorExtensions } from "@/extensions";
|
||||
@@ -11,13 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
||||
// props
|
||||
import { CoreReadOnlyEditorProps } from "@/props";
|
||||
// types
|
||||
import type {
|
||||
EditorReadOnlyRefApi,
|
||||
TExtensions,
|
||||
TDocumentEventsServer,
|
||||
TFileHandler,
|
||||
TReadOnlyMentionHandler,
|
||||
} from "@/types";
|
||||
import type { EditorReadOnlyRefApi, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
|
||||
|
||||
interface CustomReadOnlyEditorProps {
|
||||
disabledExtensions: TExtensions[];
|
||||
@@ -46,8 +40,10 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
provider,
|
||||
} = props;
|
||||
|
||||
const editor = useCustomEditor({
|
||||
const editor = useTiptapEditor({
|
||||
editable: false,
|
||||
immediatelyRender: true,
|
||||
shouldRerenderOnTransaction: false,
|
||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
||||
editorProps: {
|
||||
...CoreReadOnlyEditorProps({
|
||||
@@ -77,23 +73,21 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" });
|
||||
}, [editor, initialValue]);
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
clearEditor: (emitUpdate = false) => {
|
||||
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
},
|
||||
getMarkDown: (): string => {
|
||||
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
|
||||
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
||||
return markdownOutput;
|
||||
},
|
||||
getDocument: () => {
|
||||
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
|
||||
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editorRef.current?.getJSON() ?? null;
|
||||
const documentHTML = editor?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editor?.getJSON() ?? null;
|
||||
|
||||
return {
|
||||
binary: documentBinary,
|
||||
@@ -102,35 +96,22 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
};
|
||||
},
|
||||
scrollSummary: (marking: IMarking): void => {
|
||||
if (!editorRef.current) return;
|
||||
scrollSummary(editorRef.current, marking);
|
||||
if (!editor) return;
|
||||
scrollSummary(editor, marking);
|
||||
},
|
||||
getDocumentInfo: () => ({
|
||||
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editorRef?.current?.state),
|
||||
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
|
||||
}),
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
|
||||
// Subscribe to update event emitted from headers extension
|
||||
editorRef.current?.on("update", () => {
|
||||
callback(editorRef.current?.storage.headingList.headings);
|
||||
});
|
||||
// Return a function to unsubscribe to the continuous transactions of
|
||||
// the editor on unmounting the component that has subscribed to this
|
||||
// method
|
||||
return () => {
|
||||
editorRef.current?.off("update");
|
||||
getDocumentInfo: () => {
|
||||
if (!editor) return;
|
||||
return {
|
||||
characters: editor.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editor.state),
|
||||
words: editor.storage?.characterCount?.words?.() ?? 0,
|
||||
};
|
||||
},
|
||||
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
|
||||
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
|
||||
getHeadings: () => editorRef?.current?.storage.headingList.headings,
|
||||
}));
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
editorRef.current = editor;
|
||||
return editor;
|
||||
};
|
||||
|
||||
@@ -86,10 +86,6 @@ export type EditorReadOnlyRefApi = {
|
||||
paragraphs: number;
|
||||
words: number;
|
||||
};
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
|
||||
getHeadings: () => IMarking[];
|
||||
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
|
||||
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
|
||||
};
|
||||
|
||||
export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||
@@ -105,6 +101,10 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||
getSelectedText: () => string | null;
|
||||
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
|
||||
setProviderDocument: (value: Uint8Array) => void;
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
|
||||
getHeadings: () => IMarking[];
|
||||
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
|
||||
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
|
||||
}
|
||||
|
||||
// editor props
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: rgb(var(--color-text-400));
|
||||
color: var(--color-placeholder);
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
.ProseMirror p.is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: rgb(var(--color-text-400));
|
||||
color: var(--color-placeholder);
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
@@ -105,14 +105,14 @@ ul[data-type="taskList"] li > div {
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
border: 1px solid rgba(var(--color-border-300)) !important;
|
||||
border: 1px solid rgba(var(--color-text-100), 0.2) !important;
|
||||
outline: none;
|
||||
border-radius: 2px;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.ProseMirror[contenteditable="true"] input[type="checkbox"]:hover {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
background-color: rgba(var(--color-text-100), 0.1);
|
||||
}
|
||||
|
||||
.ProseMirror[contenteditable="false"] input[type="checkbox"] {
|
||||
@@ -145,7 +145,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
position: relative;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: rgb(var(--color-background-100));
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: 0.8rem;
|
||||
@@ -192,7 +192,7 @@ ul[data-type="taskList"] li > div {
|
||||
|
||||
ul[data-type="taskList"] li[data-checked="true"] {
|
||||
& > div > p.editor-paragraph-block {
|
||||
color: rgb(var(--color-text-400));
|
||||
color: var(--color-placeholder);
|
||||
}
|
||||
|
||||
[data-text-color] {
|
||||
|
||||
@@ -1,42 +1,6 @@
|
||||
:root {
|
||||
/* text colors */
|
||||
--editor-colors-gray-text: #5c5e63;
|
||||
--editor-colors-peach-text: #ff5b59;
|
||||
--editor-colors-pink-text: #f65385;
|
||||
--editor-colors-orange-text: #fd9038;
|
||||
--editor-colors-green-text: #0fc27b;
|
||||
--editor-colors-light-blue-text: #17bee9;
|
||||
--editor-colors-dark-blue-text: #266df0;
|
||||
--editor-colors-purple-text: #9162f9;
|
||||
/* end text colors */
|
||||
}
|
||||
|
||||
/* text background colors */
|
||||
[data-theme="light"],
|
||||
[data-theme="light-contrast"] {
|
||||
--editor-colors-gray-background: #d6d6d8;
|
||||
--editor-colors-peach-background: #ffd5d7;
|
||||
--editor-colors-pink-background: #fdd4e3;
|
||||
--editor-colors-orange-background: #ffe3cd;
|
||||
--editor-colors-green-background: #c3f0de;
|
||||
--editor-colors-light-blue-background: #c5eff9;
|
||||
--editor-colors-dark-blue-background: #c9dafb;
|
||||
--editor-colors-purple-background: #e3d8fd;
|
||||
}
|
||||
[data-theme="dark"],
|
||||
[data-theme="dark-contrast"] {
|
||||
--editor-colors-gray-background: #404144;
|
||||
--editor-colors-peach-background: #593032;
|
||||
--editor-colors-pink-background: #562e3d;
|
||||
--editor-colors-orange-background: #583e2a;
|
||||
--editor-colors-green-background: #1d4a3b;
|
||||
--editor-colors-light-blue-background: #1f495c;
|
||||
--editor-colors-dark-blue-background: #223558;
|
||||
--editor-colors-purple-background: #3d325a;
|
||||
}
|
||||
/* end text background colors */
|
||||
|
||||
.editor-container {
|
||||
--color-placeholder: rgba(var(--color-text-100), 0.5);
|
||||
|
||||
/* font sizes and line heights */
|
||||
&.large-font {
|
||||
--font-size-h1: 1.75rem;
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/utils": "*"
|
||||
"@plane/utils": "*",
|
||||
"intl-messageformat": "^10.7.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { observer } from "mobx-react";
|
||||
import React, { createContext, useEffect } from "react";
|
||||
import { Language, languages } from "../config";
|
||||
import { TranslationStore } from "./store";
|
||||
|
||||
// Create the store instance
|
||||
const translationStore = new TranslationStore();
|
||||
|
||||
// Create Context
|
||||
export const TranslationContext = createContext<TranslationStore>(translationStore);
|
||||
|
||||
export const TranslationProvider = observer(({ children }: { children: React.ReactNode }) => {
|
||||
// Handle storage events for cross-tab synchronization
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.key === "userLanguage" && event.newValue) {
|
||||
const newLang = event.newValue as Language;
|
||||
if (languages.includes(newLang)) {
|
||||
translationStore.setLanguage(newLang);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
return () => window.removeEventListener("storage", handleStorageChange);
|
||||
}, []);
|
||||
|
||||
return <TranslationContext.Provider value={translationStore}>{children}</TranslationContext.Provider>;
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { Language, fallbackLng, languages, translations } from "../config";
|
||||
|
||||
export class TranslationStore {
|
||||
currentLocale: Language = fallbackLng;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
currentLocale: observable.ref,
|
||||
});
|
||||
this.initializeLanguage();
|
||||
}
|
||||
|
||||
get availableLanguages() {
|
||||
return languages;
|
||||
}
|
||||
|
||||
t(key: string) {
|
||||
return translations[this.currentLocale]?.[key] || translations[fallbackLng][key] || key;
|
||||
}
|
||||
|
||||
setLanguage(lng: Language) {
|
||||
try {
|
||||
localStorage.setItem("userLanguage", lng);
|
||||
this.currentLocale = lng;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
initializeLanguage() {
|
||||
if (typeof window === "undefined") return;
|
||||
const savedLocale = localStorage.getItem("userLanguage") as Language;
|
||||
if (savedLocale && languages.includes(savedLocale)) {
|
||||
this.setLanguage(savedLocale);
|
||||
} else {
|
||||
const browserLang = navigator.language.split("-")[0] as Language;
|
||||
const newLocale = languages.includes(browserLang as Language) ? (browserLang as Language) : fallbackLng;
|
||||
this.setLanguage(newLocale);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import en from "../locales/en/translations.json";
|
||||
import fr from "../locales/fr/translations.json";
|
||||
import es from "../locales/es/translations.json";
|
||||
import ja from "../locales/ja/translations.json";
|
||||
|
||||
export type Language = (typeof languages)[number];
|
||||
export type Translations = {
|
||||
[key: string]: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const fallbackLng = "en";
|
||||
export const languages = ["en", "fr", "es", "ja"] as const;
|
||||
export const translations: Translations = {
|
||||
en,
|
||||
fr,
|
||||
es,
|
||||
ja,
|
||||
};
|
||||
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
{
|
||||
label: "English",
|
||||
value: "en",
|
||||
},
|
||||
{
|
||||
label: "French",
|
||||
value: "fr",
|
||||
},
|
||||
{
|
||||
label: "Spanish",
|
||||
value: "es",
|
||||
},
|
||||
{
|
||||
label: "Japanese",
|
||||
value: "ja",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./language";
|
||||
@@ -0,0 +1,13 @@
|
||||
import { TLanguage, ILanguageOption } from "../types";
|
||||
|
||||
export const FALLBACK_LANGUAGE: TLanguage = "en";
|
||||
|
||||
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "Français", value: "fr" },
|
||||
{ label: "Español", value: "es" },
|
||||
{ label: "日本語", value: "ja" },
|
||||
{ label: "中文", value: "zh-CN" },
|
||||
];
|
||||
|
||||
export const STORAGE_KEY = "userLanguage";
|
||||
@@ -0,0 +1,19 @@
|
||||
import { observer } from "mobx-react";
|
||||
import React, { createContext } from "react";
|
||||
// store
|
||||
import { TranslationStore } from "../store";
|
||||
|
||||
export const TranslationContext = createContext<TranslationStore | null>(null);
|
||||
|
||||
interface TranslationProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the translation store to the application
|
||||
*/
|
||||
export const TranslationProvider: React.FC<TranslationProviderProps> = observer(({ children }) => {
|
||||
const [store] = React.useState(() => new TranslationStore());
|
||||
|
||||
return <TranslationContext.Provider value={store}>{children}</TranslationContext.Provider>;
|
||||
});
|
||||
@@ -1,17 +1,35 @@
|
||||
import { useContext } from "react";
|
||||
import { TranslationContext } from "../components";
|
||||
import { Language } from "../config";
|
||||
import { useContext } from 'react';
|
||||
// context
|
||||
import { TranslationContext } from '../context';
|
||||
// types
|
||||
import { ILanguageOption, TLanguage } from '../types';
|
||||
|
||||
export function useTranslation() {
|
||||
export type TTranslationStore = {
|
||||
t: (key: string, params?: Record<string, any>) => string;
|
||||
currentLocale: TLanguage;
|
||||
changeLanguage: (lng: TLanguage) => void;
|
||||
languages: ILanguageOption[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides the translation store to the application
|
||||
* @returns {TTranslationStore}
|
||||
* @returns {(key: string, params?: Record<string, any>) => string} t: method to translate the key with params
|
||||
* @returns {TLanguage} currentLocale - current locale language
|
||||
* @returns {(lng: TLanguage) => void} changeLanguage - method to change the language
|
||||
* @returns {ILanguageOption[]} languages - available languages
|
||||
* @throws {Error} if the TranslationProvider is not used
|
||||
*/
|
||||
export function useTranslation(): TTranslationStore {
|
||||
const store = useContext(TranslationContext);
|
||||
if (!store) {
|
||||
throw new Error("useTranslation must be used within a TranslationProvider");
|
||||
throw new Error('useTranslation must be used within a TranslationProvider');
|
||||
}
|
||||
|
||||
return {
|
||||
t: (key: string) => store.t(key),
|
||||
t: store.t.bind(store),
|
||||
currentLocale: store.currentLocale,
|
||||
changeLanguage: (lng: Language) => store.setLanguage(lng),
|
||||
changeLanguage: (lng: TLanguage) => store.setLanguage(lng),
|
||||
languages: store.availableLanguages,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./config";
|
||||
export * from "./components";
|
||||
export * from "./constants";
|
||||
export * from "./context";
|
||||
export * from "./hooks";
|
||||
export * from "./types";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,856 @@
|
||||
{
|
||||
"submit": "提交",
|
||||
"cancel": "取消",
|
||||
"loading": "加载中",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"info": "信息",
|
||||
"close": "关闭",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"ok": "确定",
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"search": "搜索",
|
||||
"add_member": "添加成员",
|
||||
"remove_member": "移除成员",
|
||||
"add_members": "添加成员",
|
||||
"remove_members": "移除成员",
|
||||
"add": "添加",
|
||||
"remove": "移除",
|
||||
"add_new": "新增",
|
||||
"remove_selected": "移除选中项",
|
||||
"first_name": "名字",
|
||||
"last_name": "姓氏",
|
||||
"email": "电子邮件",
|
||||
"display_name": "显示名称",
|
||||
"role": "角色",
|
||||
"timezone": "时区",
|
||||
"avatar": "头像",
|
||||
"cover_image": "封面图片",
|
||||
"password": "密码",
|
||||
"change_cover": "更换封面",
|
||||
"language": "语言",
|
||||
"saving": "保存中...",
|
||||
"save_changes": "保存更改",
|
||||
"deactivate_account": "停用账户",
|
||||
"deactivate_account_description": "停用账户后,该账户内的所有数据和资源将被永久删除,且无法恢复。",
|
||||
"profile_settings": "个人资料设置",
|
||||
"your_account": "账户",
|
||||
"profile": "个人资料",
|
||||
"security": "安全",
|
||||
"activity": "活动",
|
||||
"appearance": "外观",
|
||||
"notifications": "通知",
|
||||
"workspaces": "工作区",
|
||||
"create_workspace": "创建工作区",
|
||||
"invitations": "邀请",
|
||||
"summary": "摘要",
|
||||
"assigned": "已分配",
|
||||
"created": "已创建",
|
||||
"subscribed": "已订阅",
|
||||
"you_do_not_have_the_permission_to_access_this_page": "您没有权限访问此页面。",
|
||||
"failed_to_sign_out_please_try_again": "登出失败,请重试。",
|
||||
"password_changed_successfully": "密码已成功更改。",
|
||||
"something_went_wrong_please_try_again": "出错了,请重试。",
|
||||
"change_password": "更改密码",
|
||||
"passwords_dont_match": "密码不匹配",
|
||||
"current_password": "当前密码",
|
||||
"new_password": "新密码",
|
||||
"confirm_password": "确认密码",
|
||||
"this_field_is_required": "此字段为必填项",
|
||||
"changing_password": "正在更改密码",
|
||||
"please_enter_your_password": "请输入您的密码。",
|
||||
"password_length_should_me_more_than_8_characters": "密码长度应大于8个字符。",
|
||||
"password_is_weak": "密码强度较弱。",
|
||||
"password_is_strong": "密码强度较强。",
|
||||
"load_more": "加载更多",
|
||||
"select_or_customize_your_interface_color_scheme": "选择或自定义界面配色方案。",
|
||||
"theme": "主题",
|
||||
"system_preference": "系统偏好",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"light_contrast": "浅色高对比",
|
||||
"dark_contrast": "深色高对比",
|
||||
"custom": "自定义主题",
|
||||
"select_your_theme": "选择主题",
|
||||
"customize_your_theme": "自定义主题",
|
||||
"background_color": "背景颜色",
|
||||
"text_color": "文字颜色",
|
||||
"primary_color": "主色调",
|
||||
"sidebar_background_color": "侧边栏背景颜色",
|
||||
"sidebar_text_color": "侧边栏文字颜色",
|
||||
"set_theme": "设置主题",
|
||||
"enter_a_valid_hex_code_of_6_characters": "请输入6位有效的十六进制代码",
|
||||
"background_color_is_required": "背景颜色为必填项",
|
||||
"text_color_is_required": "文字颜色为必填项",
|
||||
"primary_color_is_required": "主色调为必填项",
|
||||
"sidebar_background_color_is_required": "侧边栏背景颜色为必填项",
|
||||
"sidebar_text_color_is_required": "侧边栏文字颜色为必填项",
|
||||
"updating_theme": "正在更新主题",
|
||||
"theme_updated_successfully": "主题已成功更新",
|
||||
"failed_to_update_the_theme": "主题更新失败",
|
||||
"email_notifications": "电子邮件通知",
|
||||
"stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "启用此功能以接收您订阅问题的通知。",
|
||||
"email_notification_setting_updated_successfully": "电子邮件通知设置已成功更新",
|
||||
"failed_to_update_email_notification_setting": "电子邮件通知设置更新失败",
|
||||
"notify_me_when": "在以下情况下通知我",
|
||||
"property_changes": "属性更改",
|
||||
"property_changes_description": "当负责人、优先级、估算等属性更改时通知。",
|
||||
"state_change": "状态更改",
|
||||
"state_change_description": "当问题状态更改时通知",
|
||||
"issue_completed": "问题完成",
|
||||
"issue_completed_description": "仅在问题完成时通知",
|
||||
"comments": "评论",
|
||||
"comments_description": "当有人在问题中添加评论时通知",
|
||||
"mentions": "提及",
|
||||
"mentions_description": "仅在评论或描述中提及您时通知",
|
||||
"create_your_workspace": "创建工作区",
|
||||
"only_your_instance_admin_can_create_workspaces": "只有实例管理员可以创建工作区",
|
||||
"only_your_instance_admin_can_create_workspaces_description": "如果您知道实例管理员的电子邮件地址,请点击以下按钮联系他们。",
|
||||
"go_back": "返回",
|
||||
"request_instance_admin": "请求实例管理员",
|
||||
"plane_logo": "Plane 徽标",
|
||||
"workspace_creation_disabled": "工作区创建已禁用",
|
||||
"workspace_request_subject": "新工作区请求",
|
||||
"workspace_request_body": "实例管理员:\n\n请在 URL [/workspace-name] 处创建一个新的工作区。[工作区创建目的]\n\n谢谢。\n{{firstName}} {{lastName}}\n{{email}}",
|
||||
"creating_workspace": "正在创建工作区",
|
||||
"workspace_created_successfully": "工作区已成功创建",
|
||||
"create_workspace_page": "创建工作区页面",
|
||||
"workspace_could_not_be_created_please_try_again": "无法创建工作区,请重试。",
|
||||
"workspace_could_not_be_created_please_try_again_description": "创建工作区时出错,请重试。",
|
||||
"this_is_a_required_field": "此字段为必填项。",
|
||||
"name_your_workspace": "为工作区命名",
|
||||
"workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "工作区名称只能包含空格、破折号和字母数字字符。",
|
||||
"limit_your_name_to_80_characters": "名称长度限制为80个字符。",
|
||||
"set_your_workspace_url": "设置工作区URL",
|
||||
"limit_your_url_to_48_characters": "URL长度限制为48个字符。",
|
||||
"how_many_people_will_use_this_workspace": "有多少人将使用此工作区?",
|
||||
"how_many_people_will_use_this_workspace_description": "这将帮助确定购买的席位数量。",
|
||||
"select_a_range": "选择一个范围",
|
||||
"urls_can_contain_only_dash_and_alphanumeric_characters": "URL只能包含破折号和字母数字字符。",
|
||||
"something_familiar_and_recognizable_is_always_best": "熟悉且易于识别的名称总是最佳选择。",
|
||||
"workspace_url_is_already_taken": "工作区URL已被占用!",
|
||||
"old_password": "旧密码",
|
||||
"general_settings": "常规设置",
|
||||
"sign_out": "登出",
|
||||
"signing_out": "正在登出",
|
||||
"active_cycles": "活跃周期",
|
||||
"active_cycles_description": "监控所有项目的周期,跟踪高优先级问题,并关注需要关注的周期。",
|
||||
"on_demand_snapshots_of_all_your_cycles": "所有周期的按需快照",
|
||||
"upgrade": "升级",
|
||||
"10000_feet_view": "所有活跃周期的概览",
|
||||
"10000_feet_view_description": "放大查看所有项目的周期,而不是单独查看每个项目的周期。",
|
||||
"get_snapshot_of_each_active_cycle": "获取每个活跃周期的快照",
|
||||
"get_snapshot_of_each_active_cycle_description": "跟踪所有活跃周期的高级指标,查看进度并了解范围与截止日期的关系。",
|
||||
"compare_burndowns": "比较燃尽图",
|
||||
"compare_burndowns_description": "监控每个团队的表现,查看每个周期的燃尽报告。",
|
||||
"quickly_see_make_or_break_issues": "快速查看关键问题",
|
||||
"quickly_see_make_or_break_issues_description": "预览每个周期中与截止日期相关的高优先级问题。一键查看所有周期的详细信息。",
|
||||
"zoom_into_cycles_that_need_attention": "关注需要关注的周期",
|
||||
"zoom_into_cycles_that_need_attention_description": "一键调查未达预期的周期状态。",
|
||||
"stay_ahead_of_blockers": "提前解决阻碍",
|
||||
"stay_ahead_of_blockers_description": "发现项目间的挑战,查看其他视图中不明显的周期依赖关系。",
|
||||
"analytics": "分析",
|
||||
"workspace_invites": "工作区邀请",
|
||||
"workspace_settings": {
|
||||
"label": "工作区设置",
|
||||
"settings": {
|
||||
"general": {
|
||||
"title": "常规"
|
||||
},
|
||||
"members": {
|
||||
"title": "会员"
|
||||
},
|
||||
"billing-and-plans": {
|
||||
"title": "账单与计划"
|
||||
},
|
||||
"exports": {
|
||||
"title": "导出"
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "网络钩子"
|
||||
},
|
||||
"api-tokens": {
|
||||
"title": "API 令牌"
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
"api_tokens": {
|
||||
"title": "尚未创建 API 令牌",
|
||||
"description": "Plane API 可用于将您在 Plane 中的数据与任何外部系统集成。创建令牌即可开始使用。"
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "尚未添加网络钩子",
|
||||
"description": "创建网络钩子以接收实时更新并自动执行操作。"
|
||||
},
|
||||
"exports": {
|
||||
"title": "尚无导出记录",
|
||||
"description": "每次导出时,您都可以在此处找到参考副本。"
|
||||
},
|
||||
"imports": {
|
||||
"title": "尚无导入记录",
|
||||
"description": "在此处查找所有以前的导入记录并下载。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enter_god_mode": "进入上帝模式",
|
||||
"workspace_logo": "工作区徽标",
|
||||
"new_issue": "新建问题",
|
||||
"your_work": "您的工作",
|
||||
"drafts": "草稿",
|
||||
"projects": "项目",
|
||||
"views": "视图",
|
||||
"workspace": "工作区",
|
||||
"archives": "归档",
|
||||
"settings": "设置",
|
||||
"failed_to_move_favorite": "移动收藏失败",
|
||||
"your_favorites": "您的收藏",
|
||||
"no_favorites_yet": "尚无收藏",
|
||||
"create_folder": "创建文件夹",
|
||||
"new_folder": "新建文件夹",
|
||||
"favorite_updated_successfully": "收藏已成功更新",
|
||||
"favorite_created_successfully": "收藏已成功创建",
|
||||
"folder_already_exists": "文件夹已存在",
|
||||
"folder_name_cannot_be_empty": "文件夹名称不能为空",
|
||||
"something_went_wrong": "出错了",
|
||||
"failed_to_reorder_favorite": "重新排序收藏失败",
|
||||
"favorite_removed_successfully": "收藏已成功移除",
|
||||
"failed_to_create_favorite": "创建收藏失败",
|
||||
"failed_to_rename_favorite": "重命名收藏失败",
|
||||
"project_link_copied_to_clipboard": "项目链接已复制到剪贴板",
|
||||
"link_copied": "链接已复制",
|
||||
"your_projects": "您的项目",
|
||||
"add_project": "添加项目",
|
||||
"create_project": "创建项目",
|
||||
"failed_to_remove_project_from_favorites": "无法从收藏中移除项目,请重试。",
|
||||
"project_created_successfully": "项目已成功创建",
|
||||
"project_created_successfully_description": "项目已成功创建。您现在可以开始添加问题。",
|
||||
"project_cover_image_alt": "项目封面图片",
|
||||
"name_is_required": "名称为必填项",
|
||||
"title_should_be_less_than_255_characters": "标题应少于255个字符",
|
||||
"project_name": "项目名称",
|
||||
"project_id_must_be_at_least_1_character": "项目ID至少为1个字符",
|
||||
"project_id_must_be_at_most_5_characters": "项目ID最多为5个字符",
|
||||
"project_id": "项目ID",
|
||||
"project_id_tooltip_content": "用于唯一标识项目中的问题。最多5个字符。",
|
||||
"description_placeholder": "描述...",
|
||||
"only_alphanumeric_non_latin_characters_allowed": "仅允许字母数字和非拉丁字符。",
|
||||
"project_id_is_required": "项目ID为必填项",
|
||||
"select_network": "选择网络",
|
||||
"lead": "负责人",
|
||||
"private": "私有",
|
||||
"public": "公开",
|
||||
"accessible_only_by_invite": "仅限邀请访问",
|
||||
"anyone_in_the_workspace_except_guests_can_join": "除访客外,工作区中的任何人都可以加入",
|
||||
"creating": "创建中",
|
||||
"creating_project": "正在创建项目",
|
||||
"adding_project_to_favorites": "正在将项目添加到收藏",
|
||||
"project_added_to_favorites": "项目已添加到收藏",
|
||||
"couldnt_add_the_project_to_favorites": "无法将项目添加到收藏,请重试。",
|
||||
"removing_project_from_favorites": "正在从收藏中移除项目",
|
||||
"project_removed_from_favorites": "项目已从收藏中移除",
|
||||
"couldnt_remove_the_project_from_favorites": "无法从收藏中移除项目,请重试。",
|
||||
"add_to_favorites": "添加到收藏",
|
||||
"remove_from_favorites": "从收藏中移除",
|
||||
"publish_settings": "发布设置",
|
||||
"publish": "发布",
|
||||
"copy_link": "复制链接",
|
||||
"leave_project": "离开项目",
|
||||
"join_the_project_to_rearrange": "加入项目以重新排序",
|
||||
"drag_to_rearrange": "拖动以重新排序",
|
||||
"congrats": "恭喜!",
|
||||
"open_project": "打开项目",
|
||||
"issues": "问题",
|
||||
"cycles": "周期",
|
||||
"modules": "模块",
|
||||
"pages": "页面",
|
||||
"intake": "接收",
|
||||
"time_tracking": "时间跟踪",
|
||||
"work_management": "工作管理",
|
||||
"projects_and_issues": "项目与问题",
|
||||
"projects_and_issues_description": "在此项目上启用或禁用。",
|
||||
"cycles_description": "按时间段对项目工作进行时间盒管理,并调整频率。",
|
||||
"modules_description": "将工作分组为类似子项目的设置,拥有自己的负责人和分配人。",
|
||||
"views_description": "保存排序、过滤和显示选项以供以后使用或共享。",
|
||||
"pages_description": "写下一些内容。",
|
||||
"intake_description": "启用此功能以接收您订阅问题的通知。",
|
||||
"time_tracking_description": "跟踪问题和项目所花费的时间。",
|
||||
"work_management_description": "轻松管理工作和项目。",
|
||||
"documentation": "文档",
|
||||
"message_support": "联系支持",
|
||||
"contact_sales": "联系销售",
|
||||
"hyper_mode": "超速模式",
|
||||
"keyboard_shortcuts": "键盘快捷键",
|
||||
"whats_new": "新功能",
|
||||
"version": "版本",
|
||||
"we_are_having_trouble_fetching_the_updates": "获取更新时出现问题。",
|
||||
"our_changelogs": "我们的更新日志",
|
||||
"for_the_latest_updates": "获取最新更新,请访问",
|
||||
"please_visit": "请访问",
|
||||
"docs": "文档",
|
||||
"full_changelog": "完整更新日志",
|
||||
"support": "支持",
|
||||
"discord": "Discord",
|
||||
"powered_by_plane_pages": "由 Plane Pages 提供支持",
|
||||
"please_select_at_least_one_invitation": "请至少选择一个邀请。",
|
||||
"please_select_at_least_one_invitation_description": "请至少选择一个邀请以加入工作区。",
|
||||
"we_see_that_someone_has_invited_you_to_join_a_workspace": "我们发现有人邀请您加入一个工作区",
|
||||
"join_a_workspace": "加入工作区",
|
||||
"we_see_that_someone_has_invited_you_to_join_a_workspace_description": "我们发现有人邀请您加入一个工作区",
|
||||
"join_a_workspace_description": "加入工作区",
|
||||
"accept_and_join": "接受并加入",
|
||||
"go_home": "返回首页",
|
||||
"no_pending_invites": "没有待处理的邀请",
|
||||
"you_can_see_here_if_someone_invites_you_to_a_workspace": "如果有人邀请您加入工作区,您可以在此处查看",
|
||||
"back_to_home": "返回首页",
|
||||
"workspace_name": "工作区名称",
|
||||
"deactivate_your_account": "停用您的账户",
|
||||
"deactivate_your_account_description": "停用后,您将无法再分配问题,工作区的计费也将停止。要重新激活账户,您需要使用此电子邮件地址收到工作区邀请。",
|
||||
"deactivating": "正在停用",
|
||||
"confirm": "确认",
|
||||
"draft_created": "草稿已创建",
|
||||
"issue_created_successfully": "问题已成功创建",
|
||||
"draft_creation_failed": "草稿创建失败",
|
||||
"issue_creation_failed": "问题创建失败",
|
||||
"draft_issue": "草稿问题",
|
||||
"issue_updated_successfully": "问题已成功更新",
|
||||
"issue_could_not_be_updated": "无法更新问题",
|
||||
"create_a_draft": "创建草稿",
|
||||
"save_to_drafts": "保存为草稿",
|
||||
"save": "保存",
|
||||
"updating": "正在更新",
|
||||
"create_new_issue": "创建新问题",
|
||||
"editor_is_not_ready_to_discard_changes": "编辑器尚未准备好丢弃更改",
|
||||
"failed_to_move_issue_to_project": "无法将问题移动到项目",
|
||||
"create_more": "创建更多",
|
||||
"add_to_project": "添加到项目",
|
||||
"discard": "丢弃",
|
||||
"duplicate_issue_found": "发现重复问题",
|
||||
"duplicate_issues_found": "发现重复问题",
|
||||
"no_matching_results": "没有匹配的结果",
|
||||
"title_is_required": "标题为必填项",
|
||||
"title": "标题",
|
||||
"state": "状态",
|
||||
"priority": "优先级",
|
||||
"none": "无",
|
||||
"urgent": "紧急",
|
||||
"high": "高",
|
||||
"medium": "中",
|
||||
"low": "低",
|
||||
"members": "成员",
|
||||
"assignee": "分配人",
|
||||
"assignees": "分配人",
|
||||
"you": "您",
|
||||
"labels": "标签",
|
||||
"create_new_label": "创建新标签",
|
||||
"start_date": "开始日期",
|
||||
"due_date": "截止日期",
|
||||
"cycle": "周期",
|
||||
"estimate": "估算",
|
||||
"change_parent_issue": "更改父问题",
|
||||
"remove_parent_issue": "移除父问题",
|
||||
"add_parent": "添加父问题",
|
||||
"loading_members": "正在加载成员...",
|
||||
"inbox": "收件箱",
|
||||
"show_all": "显示全部",
|
||||
"show_less": "显示较少",
|
||||
"no_data_yet": "暂无数据",
|
||||
|
||||
"project_view": {
|
||||
"access": {
|
||||
"public": "公开",
|
||||
"private": "私密"
|
||||
},
|
||||
"sort_by": {
|
||||
"created_at": "创建时间",
|
||||
"updated_at": "更新时间",
|
||||
"name": "名称"
|
||||
}
|
||||
},
|
||||
|
||||
"toast": {
|
||||
"success": "成功!",
|
||||
"error": "错误!"
|
||||
},
|
||||
|
||||
"home": {
|
||||
"empty": {
|
||||
"create_project": {
|
||||
"title": "创建项目",
|
||||
"description": "在Plane中,大多数事情都从项目开始。",
|
||||
"cta": "开始使用"
|
||||
},
|
||||
"invite_team": {
|
||||
"title": "邀请团队",
|
||||
"description": "与同事一起构建、部署和管理。",
|
||||
"cta": "邀请成员"
|
||||
},
|
||||
"configure_workspace": {
|
||||
"title": "设置工作空间",
|
||||
"description": "开启或关闭功能,或进行更多设置。",
|
||||
"cta": "配置此工作空间"
|
||||
},
|
||||
"personalize_account": {
|
||||
"title": "让Plane更个性化",
|
||||
"description": "选择您的头像、颜色等更多设置。",
|
||||
"cta": "立即个性化"
|
||||
}
|
||||
},
|
||||
"quick_links": {
|
||||
"empty": "保存您想随手可得的工作相关链接。",
|
||||
"add": "添加快捷链接",
|
||||
"title": "快捷链接",
|
||||
"title_plural": "快捷链接",
|
||||
"toasts": {
|
||||
"created": {
|
||||
"title": "链接已创建",
|
||||
"message": "链接已成功创建"
|
||||
},
|
||||
"not_created": {
|
||||
"title": "链接未创建",
|
||||
"message": "无法创建链接"
|
||||
},
|
||||
"updated": {
|
||||
"title": "链接已更新",
|
||||
"message": "链接已成功更新"
|
||||
},
|
||||
"not_updated": {
|
||||
"title": "链接未更新",
|
||||
"message": "无法更新链接"
|
||||
},
|
||||
"removed": {
|
||||
"title": "链接已删除",
|
||||
"message": "链接已成功删除"
|
||||
},
|
||||
"not_removed": {
|
||||
"title": "链接未删除",
|
||||
"message": "无法删除链接"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recents": {
|
||||
"title": "最近",
|
||||
"empty": {
|
||||
"project": "访问项目后,您的最近项目将显示在这里。",
|
||||
"page": "访问页面后,您的最近页面将显示在这里。",
|
||||
"issue": "访问问题后,您的最近问题将显示在这里。",
|
||||
"default": "您还没有任何最近项目。"
|
||||
},
|
||||
"filters": {
|
||||
"all": "所有项目",
|
||||
"projects": "项目",
|
||||
"pages": "页面",
|
||||
"issues": "问题"
|
||||
}
|
||||
},
|
||||
"my_stickies": {
|
||||
"title": "您的便签"
|
||||
},
|
||||
"new_at_plane": {
|
||||
"title": "Plane 新功能"
|
||||
},
|
||||
"quick_tutorial": {
|
||||
"title": "快速教程"
|
||||
},
|
||||
"widget": {
|
||||
"reordered_successfully": "小部件重新排序成功。",
|
||||
"reordering_failed": "重新排序小部件时发生错误。"
|
||||
},
|
||||
"manage_widgets": "管理小部件",
|
||||
"title": "首页",
|
||||
"star_us_on_github": "在 GitHub 上为我们加星"
|
||||
},
|
||||
|
||||
"link": {
|
||||
"modal": {
|
||||
"url": {
|
||||
"text": "网址",
|
||||
"required": "网址无效",
|
||||
"placeholder": "输入或粘贴网址"
|
||||
},
|
||||
"title": {
|
||||
"text": "显示标题",
|
||||
"placeholder": "您希望将此链接显示为什么"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"all": "全部",
|
||||
"states": "状态",
|
||||
"state": "状态",
|
||||
"state_groups": "状态组",
|
||||
"priority": "优先级",
|
||||
"team_project": "团队项目",
|
||||
"project": "项目",
|
||||
"cycle": "周期",
|
||||
"cycles": "周期",
|
||||
"module": "模块",
|
||||
"modules": "模块",
|
||||
"labels": "标签",
|
||||
"assignees": "分配人",
|
||||
"assignee": "分配人",
|
||||
"created_by": "创建者",
|
||||
"none": "无",
|
||||
"link": "链接",
|
||||
"estimate": "估算",
|
||||
"layout": "布局",
|
||||
"filters": "筛选",
|
||||
"display": "显示",
|
||||
"load_more": "加载更多",
|
||||
"no_matches_found": "未找到匹配项",
|
||||
"activity": "活动",
|
||||
"analytics": "分析",
|
||||
"success": "成功",
|
||||
"error": "错误",
|
||||
"group_by": "分组方式",
|
||||
"search": "搜索",
|
||||
"epic": "史诗",
|
||||
"issue": "问题",
|
||||
"warning": "警告",
|
||||
"updating": "更新中",
|
||||
"update": "更新",
|
||||
"creating": "创建中",
|
||||
"cancel": "取消",
|
||||
"description": "描述",
|
||||
"title": "标题",
|
||||
"order_by": {
|
||||
"label": "排序方式",
|
||||
"manual": "手动",
|
||||
"last_created": "最近创建",
|
||||
"last_updated": "最近更新",
|
||||
"start_date": "开始日期",
|
||||
"due_date": "截止日期",
|
||||
"asc": "升序",
|
||||
"desc": "降序"
|
||||
},
|
||||
"sort": {
|
||||
"asc": "升序",
|
||||
"desc": "降序"
|
||||
},
|
||||
"comments": "评论",
|
||||
"updates": "更新"
|
||||
},
|
||||
"form": {
|
||||
"title": {
|
||||
"required": "标题为必填项",
|
||||
"max_length": "标题不能超过{length}个字符"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"grouping_title": "{entity}分组",
|
||||
"priority": "{entity}",
|
||||
"all": "所有{entity}",
|
||||
"drop_here_to_move": "拖放至此以移动{entity}"
|
||||
},
|
||||
"epic": {
|
||||
"all": "所有史诗",
|
||||
"label": "{count, plural, one {史诗} other {史诗}}"
|
||||
},
|
||||
"issue": {
|
||||
"label": "{count, plural, one {问题} other {问题}}",
|
||||
"all": "所有问题",
|
||||
"add": "添加问题",
|
||||
"priority": {
|
||||
"urgent": "紧急",
|
||||
"high": "高",
|
||||
"medium": "中",
|
||||
"low": "低"
|
||||
},
|
||||
"display": {
|
||||
"properties": {
|
||||
"label": "显示属性",
|
||||
"id": "ID",
|
||||
"issue_type": "问题类型",
|
||||
"sub_issue_count": "子问题数量",
|
||||
"attachment_count": "附件数量"
|
||||
},
|
||||
"extra": {
|
||||
"show_sub_issues": "显示子问题",
|
||||
"show_empty_groups": "显示空分组"
|
||||
}
|
||||
},
|
||||
"layouts": {
|
||||
"ordered_by_label": "当前排序方式",
|
||||
"list": "列表",
|
||||
"kanban": "看板",
|
||||
"calendar": "日历",
|
||||
"spreadsheet": "表格",
|
||||
"gantt": "时间线",
|
||||
"title": {
|
||||
"list": "列表视图",
|
||||
"kanban": "看板视图",
|
||||
"calendar": "日历视图",
|
||||
"spreadsheet": "表格视图",
|
||||
"gantt": "时间线视图"
|
||||
}
|
||||
},
|
||||
"states": {
|
||||
"active": "进行中",
|
||||
"backlog": "待办"
|
||||
},
|
||||
"comments": {
|
||||
"create": {
|
||||
"success": "评论创建成功",
|
||||
"error": "评论创建失败,请稍后重试"
|
||||
},
|
||||
"update": {
|
||||
"success": "评论更新成功",
|
||||
"error": "评论更新失败,请稍后重试"
|
||||
},
|
||||
"remove": {
|
||||
"success": "评论删除成功",
|
||||
"error": "评论删除失败,请稍后重试"
|
||||
},
|
||||
"upload": {
|
||||
"error": "资源上传失败,请稍后重试"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"project": {
|
||||
"label": "{count, plural, one {项目} other {项目}}"
|
||||
},
|
||||
|
||||
"view": {
|
||||
"create": {
|
||||
"label": "创建视图"
|
||||
},
|
||||
"update": {
|
||||
"label": "更新视图"
|
||||
}
|
||||
},
|
||||
|
||||
"inbox_issue": {
|
||||
"status": {
|
||||
"pending": {
|
||||
"title": "待处理",
|
||||
"description": "待处理"
|
||||
},
|
||||
"declined": {
|
||||
"title": "已拒绝",
|
||||
"description": "已拒绝"
|
||||
},
|
||||
"snoozed": {
|
||||
"title": "已暂停",
|
||||
"description": "还剩{days}天"
|
||||
},
|
||||
"accepted": {
|
||||
"title": "已接受",
|
||||
"description": "已接受"
|
||||
},
|
||||
"duplicate": {
|
||||
"title": "重复",
|
||||
"description": "重复"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"in-app": "应用内"
|
||||
},
|
||||
"order_by": {
|
||||
"created_at": "创建时间",
|
||||
"updated_at": "更新时间",
|
||||
"id": "编号"
|
||||
}
|
||||
},
|
||||
|
||||
"user_profile": {
|
||||
"title": "你的工作",
|
||||
"work": "工作",
|
||||
"details": {
|
||||
"joined_on": "加入时间",
|
||||
"time_zone": "时区"
|
||||
},
|
||||
"stats": {
|
||||
"workload": "工作量",
|
||||
"overview": "概述",
|
||||
"created": "已创建问题",
|
||||
"assigned": "已分配问题",
|
||||
"subscribed": "已订阅问题",
|
||||
"state_distribution": {
|
||||
"title": "按状态显示的问题",
|
||||
"empty": "创建问题以在图表中按状态查看它们,以便更好地分析。"
|
||||
},
|
||||
"priority_distribution": {
|
||||
"title": "按优先级显示的问题",
|
||||
"empty": "创建问题以在图表中按优先级查看它们,以便更好地分析。"
|
||||
},
|
||||
"recent_activity": {
|
||||
"title": "最近活动",
|
||||
"empty": "我们找不到数据。请查看您的输入"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"profile": "个人资料",
|
||||
"security": "安全",
|
||||
"activity": "活动",
|
||||
"appearance": "外观",
|
||||
"notifications": "通知"
|
||||
},
|
||||
"tabs": {
|
||||
"summary": "摘要",
|
||||
"assigned": "已分配",
|
||||
"created": "已创建",
|
||||
"subscribed": "已订阅",
|
||||
"activity": "活动"
|
||||
}
|
||||
},
|
||||
|
||||
"role_details": {
|
||||
"guest": {
|
||||
"title": "访客",
|
||||
"description": "组织的外部成员可以被邀请为访客。"
|
||||
},
|
||||
"member": {
|
||||
"title": "成员",
|
||||
"description": "可以在项目、周期和模块内读取、写入、编辑和删除实体"
|
||||
},
|
||||
"admin": {
|
||||
"title": "管理员",
|
||||
"description": "工作区内的所有权限都设置为允许。"
|
||||
}
|
||||
},
|
||||
|
||||
"user_roles": {
|
||||
"product_or_project_manager": "产品/项目经理",
|
||||
"development_or_engineering": "开发/工程",
|
||||
"founder_or_executive": "创始人/高管",
|
||||
"freelancer_or_consultant": "自由职业者/顾问",
|
||||
"marketing_or_growth": "市场营销/增长",
|
||||
"sales_or_business_development": "销售/业务发展",
|
||||
"support_or_operations": "支持/运营",
|
||||
"student_or_professor": "学生/教授",
|
||||
"human_or_resources": "人力资源",
|
||||
"other": "其他"
|
||||
},
|
||||
|
||||
"importer": {
|
||||
"github": {
|
||||
"title": "Github",
|
||||
"description": "从 GitHub 仓库导入问题并同步。"
|
||||
},
|
||||
"jira": {
|
||||
"title": "Jira",
|
||||
"description": "从 Jira 项目导入问题和史诗。"
|
||||
}
|
||||
},
|
||||
|
||||
"exporter": {
|
||||
"csv": {
|
||||
"title": "CSV",
|
||||
"description": "将问题导出为 CSV 文件。"
|
||||
},
|
||||
"xlsx": {
|
||||
"title": "Excel",
|
||||
"description": "将问题导出为 Excel 文件。"
|
||||
},
|
||||
"json": {
|
||||
"title": "JSON",
|
||||
"description": "将问题导出为 JSON 文件。"
|
||||
}
|
||||
},
|
||||
|
||||
"default_global_view": {
|
||||
"all_issues": "所有问题",
|
||||
"assigned": "已分配",
|
||||
"created": "已创建",
|
||||
"subscribed": "已订阅"
|
||||
},
|
||||
|
||||
"stickies": {
|
||||
"title": "便签",
|
||||
"placeholder": "点击此处输入",
|
||||
"all": "所有便签",
|
||||
"no-data": "记下想法,捕捉灵感,或记录突发奇想。添加便签即可开始。",
|
||||
"add": "添加便签",
|
||||
"search_placeholder": "按标题搜索",
|
||||
"delete": "删除便签",
|
||||
"delete_confirmation": "确定要删除这个便签吗?",
|
||||
"empty_state": {
|
||||
"simple": "记下想法,捕捉灵感,或记录突发奇想。添加便签即可开始。",
|
||||
"general": {
|
||||
"title": "便签是您随手记下的快速笔记和待办事项。",
|
||||
"description": "通过创建便签轻松捕捉您的想法和创意,随时随地都可以访问。",
|
||||
"primary_button": {
|
||||
"text": "添加便签"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "没有找到匹配的便签。",
|
||||
"description": "尝试使用不同的搜索词,或如果您确定搜索正确,\n请告诉我们。",
|
||||
"primary_button": {
|
||||
"text": "添加便签"
|
||||
}
|
||||
}
|
||||
},
|
||||
"toasts": {
|
||||
"errors": {
|
||||
"wrong_name": "便签名称不能超过100个字符。",
|
||||
"already_exists": "已存在一个没有描述的便签"
|
||||
},
|
||||
"created": {
|
||||
"title": "便签已创建",
|
||||
"message": "便签已成功创建"
|
||||
},
|
||||
"not_created": {
|
||||
"title": "便签未创建",
|
||||
"message": "无法创建便签"
|
||||
},
|
||||
"updated": {
|
||||
"title": "便签已更新",
|
||||
"message": "便签已成功更新"
|
||||
},
|
||||
"not_updated": {
|
||||
"title": "便签未更新",
|
||||
"message": "无法更新便签"
|
||||
},
|
||||
"removed": {
|
||||
"title": "便签已删除",
|
||||
"message": "便签已成功删除"
|
||||
},
|
||||
"not_removed": {
|
||||
"title": "便签未删除",
|
||||
"message": "无法删除便签"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace_projects": {
|
||||
"network": {
|
||||
"private": {
|
||||
"title": "私有",
|
||||
"description": "仅限邀请访问"
|
||||
},
|
||||
"public": {
|
||||
"title": "公开",
|
||||
"description": "除访客外的工作区所有成员都可加入"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"permission": "您没有执行此操作的权限。",
|
||||
"cycle_delete": "删除周期失败",
|
||||
"module_delete": "删除模块失败",
|
||||
"issue_delete": "删除问题失败"
|
||||
},
|
||||
"state": {
|
||||
"backlog": "待办",
|
||||
"unstarted": "未开始",
|
||||
"started": "进行中",
|
||||
"completed": "已完成",
|
||||
"cancelled": "已取消"
|
||||
},
|
||||
"sort": {
|
||||
"manual": "手动",
|
||||
"name": "名称",
|
||||
"created_at": "创建日期",
|
||||
"members_length": "成员数量"
|
||||
},
|
||||
"scope": {
|
||||
"my_projects": "我的项目",
|
||||
"archived_projects": "已归档"
|
||||
},
|
||||
"common": {
|
||||
"months_count": "{months, plural, one{#个月} other{#个月}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import IntlMessageFormat from "intl-messageformat";
|
||||
import get from "lodash/get";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
// constants
|
||||
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
|
||||
// types
|
||||
import { TLanguage, ILanguageOption, ITranslations } from "../types";
|
||||
|
||||
/**
|
||||
* Mobx store class for handling translations and language changes in the application
|
||||
* Provides methods to translate keys with params and change the language
|
||||
* Uses IntlMessageFormat to format the translations
|
||||
*/
|
||||
export class TranslationStore {
|
||||
// List of translations for each language
|
||||
private translations: ITranslations = {};
|
||||
// Cache for IntlMessageFormat instances
|
||||
private messageCache: Map<string, IntlMessageFormat> = new Map();
|
||||
// Current language
|
||||
currentLocale: TLanguage = FALLBACK_LANGUAGE;
|
||||
|
||||
/**
|
||||
* Constructor for the TranslationStore class
|
||||
*/
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
this.initializeLanguage();
|
||||
this.loadTranslations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads translations from JSON files and initializes the message cache
|
||||
*/
|
||||
private async loadTranslations() {
|
||||
try {
|
||||
// dynamic import of translations
|
||||
const translations = {
|
||||
en: (await import("../locales/en/translations.json")).default,
|
||||
fr: (await import("../locales/fr/translations.json")).default,
|
||||
es: (await import("../locales/es/translations.json")).default,
|
||||
ja: (await import("../locales/ja/translations.json")).default,
|
||||
"zh-CN": (await import("../locales/zh-CN/translations.json")).default,
|
||||
};
|
||||
this.translations = translations;
|
||||
this.messageCache.clear(); // Clear cache when translations change
|
||||
} catch (error) {
|
||||
console.error("Failed to load translations:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Initializes the language based on the local storage or browser language */
|
||||
private initializeLanguage() {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const savedLocale = localStorage.getItem(STORAGE_KEY) as TLanguage;
|
||||
if (this.isValidLanguage(savedLocale)) {
|
||||
this.setLanguage(savedLocale);
|
||||
return;
|
||||
}
|
||||
|
||||
const browserLang = this.getBrowserLanguage();
|
||||
this.setLanguage(browserLang);
|
||||
}
|
||||
|
||||
/** Checks if the language is valid based on the supported languages */
|
||||
private isValidLanguage(lang: string | null): lang is TLanguage {
|
||||
return lang !== null && this.availableLanguages.some((l) => l.value === lang);
|
||||
}
|
||||
|
||||
/** Checks if a language code is similar to any supported language */
|
||||
private findSimilarLanguage(lang: string): TLanguage | null {
|
||||
// Convert to lowercase for case-insensitive comparison
|
||||
const normalizedLang = lang.toLowerCase();
|
||||
|
||||
// Find a supported language that includes or is included in the browser language
|
||||
const similarLang = this.availableLanguages.find(
|
||||
(l) => normalizedLang.includes(l.value.toLowerCase()) || l.value.toLowerCase().includes(normalizedLang)
|
||||
);
|
||||
|
||||
return similarLang ? similarLang.value : null;
|
||||
}
|
||||
|
||||
/** Gets the browser language based on the navigator.language */
|
||||
private getBrowserLanguage(): TLanguage {
|
||||
const browserLang = navigator.language;
|
||||
|
||||
// Check exact match first
|
||||
if (this.isValidLanguage(browserLang)) {
|
||||
return browserLang;
|
||||
}
|
||||
|
||||
// Check base language without region code
|
||||
const baseLang = browserLang.split("-")[0];
|
||||
if (this.isValidLanguage(baseLang)) {
|
||||
return baseLang as TLanguage;
|
||||
}
|
||||
|
||||
// Try to find a similar language
|
||||
const similarLang = this.findSimilarLanguage(browserLang) || this.findSimilarLanguage(baseLang);
|
||||
|
||||
return similarLang || FALLBACK_LANGUAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache key for the given key and locale
|
||||
* @param key - the key to get the cache key for
|
||||
* @param locale - the locale to get the cache key for
|
||||
* @returns the cache key for the given key and locale
|
||||
*/
|
||||
private getCacheKey(key: string, locale: TLanguage): string {
|
||||
return `${locale}:${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IntlMessageFormat instance for the given key and locale
|
||||
* Returns cached instance if available
|
||||
* Throws an error if the key is not found in the translations
|
||||
*/
|
||||
private getMessageInstance(key: string, locale: TLanguage): IntlMessageFormat | null {
|
||||
const cacheKey = this.getCacheKey(key, locale);
|
||||
|
||||
// Check if the cache already has the key
|
||||
if (this.messageCache.has(cacheKey)) {
|
||||
return this.messageCache.get(cacheKey) || null;
|
||||
}
|
||||
|
||||
// Get the message from the translations
|
||||
const message = get(this.translations[locale], key);
|
||||
if (!message) return null;
|
||||
|
||||
try {
|
||||
const formatter = new IntlMessageFormat(message as any, locale);
|
||||
this.messageCache.set(cacheKey, formatter);
|
||||
return formatter;
|
||||
} catch (error) {
|
||||
console.error(`Failed to create message formatter for key "${key}":`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a key with params using the current locale
|
||||
* Falls back to the default language if the translation is not found
|
||||
* Returns the key itself if the translation is not found
|
||||
* @param key - The key to translate
|
||||
* @param params - The params to format the translation with
|
||||
* @returns The translated string
|
||||
*/
|
||||
t(key: string, params?: Record<string, any>): string {
|
||||
try {
|
||||
// Try current locale
|
||||
let formatter = this.getMessageInstance(key, this.currentLocale);
|
||||
|
||||
// Fallback to default language if necessary
|
||||
if (!formatter && this.currentLocale !== FALLBACK_LANGUAGE) {
|
||||
formatter = this.getMessageInstance(key, FALLBACK_LANGUAGE);
|
||||
}
|
||||
|
||||
// If we have a formatter, use it
|
||||
if (formatter) {
|
||||
return formatter.format(params || {}) as string;
|
||||
}
|
||||
|
||||
// Last resort: return the key itself
|
||||
return key;
|
||||
} catch (error) {
|
||||
console.error(`Translation error for key "${key}":`, error);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current language and updates the translations
|
||||
* @param lng - The new language
|
||||
*/
|
||||
setLanguage(lng: TLanguage): void {
|
||||
try {
|
||||
if (!this.isValidLanguage(lng)) {
|
||||
throw new Error(`Invalid language: ${lng}`);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORAGE_KEY, lng);
|
||||
}
|
||||
this.currentLocale = lng;
|
||||
this.messageCache.clear(); // Clear cache when language changes
|
||||
if (typeof window !== "undefined") {
|
||||
document.documentElement.lang = lng;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to set language:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the available language options for the dropdown
|
||||
* @returns An array of language options
|
||||
*/
|
||||
get availableLanguages(): ILanguageOption[] {
|
||||
return SUPPORTED_LANGUAGES;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./language";
|
||||
export * from "./translation";
|
||||
@@ -0,0 +1,6 @@
|
||||
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN";
|
||||
|
||||
export interface ILanguageOption {
|
||||
label: string;
|
||||
value: TLanguage;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface ITranslation {
|
||||
[key: string]: string | ITranslation;
|
||||
}
|
||||
|
||||
export interface ITranslations {
|
||||
[locale: string]: ITranslation;
|
||||
}
|
||||
@@ -22,7 +22,6 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.6"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-3
@@ -104,6 +104,7 @@ export interface ICycle extends TProgressSnapshot {
|
||||
project_detail: IProjectDetails;
|
||||
progress: any[];
|
||||
version: number;
|
||||
pending_issues: number;
|
||||
}
|
||||
|
||||
export interface CycleIssueResponse {
|
||||
@@ -120,9 +121,7 @@ export interface CycleIssueResponse {
|
||||
sub_issues_count: number;
|
||||
}
|
||||
|
||||
export type SelectCycleType =
|
||||
| (ICycle & { actionType: "edit" | "delete" | "create-issue" })
|
||||
| undefined;
|
||||
export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
|
||||
|
||||
export type CycleDateCheckData = {
|
||||
start_date: string;
|
||||
|
||||
Vendored
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { TLogoProps } from "./common";
|
||||
import { TIssuePriorities } from "./issues";
|
||||
|
||||
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project";
|
||||
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project" | "workspace_page";
|
||||
export type THomeWidgetKeys = "quick_links" | "recents" | "my_stickies" | "quick_tutorial" | "new_at_plane";
|
||||
|
||||
export type THomeWidgetProps = {
|
||||
@@ -12,9 +12,9 @@ export type TPageEntityData = {
|
||||
id: string;
|
||||
name: string;
|
||||
logo_props: TLogoProps;
|
||||
project_id: string;
|
||||
project_id?: string;
|
||||
owned_by: string;
|
||||
project_identifier: string;
|
||||
project_identifier?: string;
|
||||
};
|
||||
|
||||
export type TProjectEntityData = {
|
||||
@@ -39,7 +39,7 @@ export type TIssueEntityData = {
|
||||
|
||||
export type TActivityEntityData = {
|
||||
id: string;
|
||||
entity_name: "page" | "project" | "issue";
|
||||
entity_name: "page" | "project" | "issue" | "workspace_page";
|
||||
entity_identifier: string;
|
||||
visited_at: string;
|
||||
entity_data: TPageEntityData | TProjectEntityData | TIssueEntityData;
|
||||
|
||||
Vendored
+1
-32
@@ -2,23 +2,6 @@ import { TPaginationInfo } from "./common";
|
||||
import { TIssuePriorities } from "./issues";
|
||||
import { TIssue } from "./issues/base";
|
||||
|
||||
enum EInboxIssueCurrentTab {
|
||||
OPEN = "open",
|
||||
CLOSED = "closed",
|
||||
}
|
||||
|
||||
enum EInboxIssueStatus {
|
||||
PENDING = -2,
|
||||
DECLINED = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
}
|
||||
|
||||
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
|
||||
|
||||
export type TInboxIssueStatus = EInboxIssueStatus;
|
||||
|
||||
// filters
|
||||
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
|
||||
|
||||
@@ -38,10 +21,7 @@ export type TInboxIssueFilter = {
|
||||
// sorting filters
|
||||
export type TInboxIssueSortingKeys = "order_by" | "sort_by";
|
||||
|
||||
export type TInboxIssueSortingOrderByKeys =
|
||||
| "issue__created_at"
|
||||
| "issue__updated_at"
|
||||
| "issue__sequence_id";
|
||||
export type TInboxIssueSortingOrderByKeys = "issue__created_at" | "issue__updated_at" | "issue__sequence_id";
|
||||
|
||||
export type TInboxIssueSortingSortByKeys = "asc" | "desc";
|
||||
|
||||
@@ -78,17 +58,6 @@ export type TInboxDuplicateIssueDetails = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export type TInboxIssuePaginationInfo = TPaginationInfo & {
|
||||
total_results: number;
|
||||
};
|
||||
|
||||
Vendored
+1
-1
@@ -15,7 +15,7 @@ import type {
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
TIssueGroupingFilters,
|
||||
TIssueExtraOptions
|
||||
TIssueExtraOptions,
|
||||
} from "@plane/types";
|
||||
|
||||
export interface IIssueCycle {
|
||||
|
||||
Vendored
+12
-4
@@ -1,8 +1,16 @@
|
||||
import { TLogoProps } from "./common";
|
||||
|
||||
export type TSticky = {
|
||||
created_at?: string | undefined;
|
||||
created_by?: string | undefined;
|
||||
background_color?: string | null | undefined;
|
||||
description?: object | undefined;
|
||||
description_html?: string | undefined;
|
||||
id: string;
|
||||
logo_props: TLogoProps | undefined;
|
||||
name?: string;
|
||||
description_html?: string;
|
||||
color?: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
sort_order: number | undefined;
|
||||
updated_at?: string | undefined;
|
||||
updated_by?: string | undefined;
|
||||
workspace: string | undefined;
|
||||
};
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
"postcss-cli": "^11.0.0",
|
||||
"postcss-nested": "^6.0.1",
|
||||
"storybook": "^8.1.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} />
|
||||
</head>
|
||||
<body>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
<AppProvider>
|
||||
<>{children}</>
|
||||
</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "./helpers";
|
||||
@@ -18,6 +19,9 @@ type Props = {
|
||||
export const FilterPriority: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
@@ -40,11 +44,11 @@ export const FilterPriority: React.FC<Props> = observer((props) => {
|
||||
isChecked={appliedFilters?.includes(priority.key) ? true : false}
|
||||
onClick={() => handleUpdate(priority.key)}
|
||||
icon={<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />}
|
||||
title={priority.title}
|
||||
title={t(priority.titleTranslationKey)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||
<p className="text-xs italic text-custom-text-400">{t("common.no_matches_found")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -94,7 +94,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
||||
<HeaderGroupByCard
|
||||
groupBy={groupBy}
|
||||
icon={subList.icon}
|
||||
icon={subList.icon as any}
|
||||
title={subList.name}
|
||||
count={getGroupIssueCount(subList.id, undefined, false) ?? 0}
|
||||
/>
|
||||
|
||||
@@ -131,10 +131,14 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
|
||||
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
|
||||
|
||||
if (subGroupByVisibilityToggle === false) return <></>;
|
||||
|
||||
return (
|
||||
<div key={`${subGroupBy}_${group.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
|
||||
<HeaderGroupByCard groupBy={groupBy} icon={group.icon} title={group.name} count={groupCount} />
|
||||
<HeaderGroupByCard
|
||||
groupBy={groupBy}
|
||||
icon={group.icon}
|
||||
title={group.name}
|
||||
count={groupCount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -262,7 +266,7 @@ const SubGroup: React.FC<ISubGroup> = observer((props) => {
|
||||
<div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
|
||||
<div className="sticky left-0 flex-shrink-0">
|
||||
<HeaderSubGroupByCard
|
||||
icon={group.icon}
|
||||
icon={group.icon as any}
|
||||
title={group.name || ""}
|
||||
count={issueCount}
|
||||
isExpanded={isExpanded}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane types
|
||||
import { IGroupByColumn, TIssueGroupByOptions, IIssueDisplayProperties, TPaginationData, TLoader } from "@plane/types";
|
||||
// plane utils
|
||||
@@ -62,6 +63,8 @@ export const ListGroup = observer((props: Props) => {
|
||||
} = props;
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const groupRef = useRef<HTMLDivElement | null>(null);
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -85,7 +88,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
}
|
||||
onClick={() => loadMoreIssues(group.id)}
|
||||
>
|
||||
Load More ↓
|
||||
{t("common.load_more")} ↓
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { TIssuePriorities } from "@plane/types";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
@@ -13,17 +14,19 @@ export const IssueBlockPriority = ({
|
||||
priority: TIssuePriorities | null;
|
||||
shouldShowName?: boolean;
|
||||
}) => {
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null;
|
||||
|
||||
if (priority_detail === null) return <></>;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priority_detail?.title}>
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
|
||||
<div className="flex items-center relative w-full h-full">
|
||||
<div className={`grid h-5 w-5 place-items-center rounded border-[0.5px] gap-2 ${priority_detail?.className}`}>
|
||||
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
|
||||
</div>
|
||||
{shouldShowName && <span className="pl-2 text-sm">{priority_detail?.title}</span>}
|
||||
{shouldShowName && <span className="pl-2 text-sm">{t(priority_detail?.titleTranslationKey || "")}</span>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,8 @@ import { observer } from "mobx-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
// ui
|
||||
import { SITES_ISSUE_LAYOUTS } from "@plane/constants";
|
||||
// plane i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
@@ -19,6 +21,8 @@ type Props = {
|
||||
|
||||
export const IssuesLayoutSelection: FC<Props> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -45,7 +49,7 @@ export const IssuesLayoutSelection: FC<Props> = observer((props) => {
|
||||
if (!layoutOptions[layout.key]) return;
|
||||
|
||||
return (
|
||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
||||
<Tooltip key={layout.key} tooltipContent={t(layout.titleTranslationKey)}>
|
||||
<button
|
||||
type="button"
|
||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { CalendarCheck2, Signal } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { getIssuePriorityFilters } from "@plane/utils";
|
||||
@@ -24,6 +25,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDetails, mode }) => {
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
const { getStateById } = useStates();
|
||||
const state = getStateById(issueDetails?.state_id ?? undefined);
|
||||
|
||||
@@ -95,7 +98,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDet
|
||||
<Icon iconName={priority?.icon} />
|
||||
</span>
|
||||
)}
|
||||
<span>{priority?.title ?? "None"}</span>
|
||||
<span>{t(priority?.titleTranslationKey || "common.none")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@mui/material": "^5.14.1",
|
||||
"@plane/constants": "*",
|
||||
"@plane/editor": "*",
|
||||
"@plane/i18n": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/services": "*",
|
||||
|
||||
@@ -300,6 +300,53 @@
|
||||
--color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */
|
||||
--color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */
|
||||
}
|
||||
|
||||
/* stickies and editor colors */
|
||||
:root {
|
||||
/* text colors */
|
||||
--editor-colors-gray-text: #5c5e63;
|
||||
--editor-colors-peach-text: #ff5b59;
|
||||
--editor-colors-pink-text: #f65385;
|
||||
--editor-colors-orange-text: #fd9038;
|
||||
--editor-colors-green-text: #0fc27b;
|
||||
--editor-colors-light-blue-text: #17bee9;
|
||||
--editor-colors-dark-blue-text: #266df0;
|
||||
--editor-colors-purple-text: #9162f9;
|
||||
/* end text colors */
|
||||
|
||||
/* background colors */
|
||||
--editor-colors-gray-background: #d6d6d8;
|
||||
--editor-colors-peach-background: #ffd5d7;
|
||||
--editor-colors-pink-background: #fdd4e3;
|
||||
--editor-colors-orange-background: #ffe3cd;
|
||||
--editor-colors-green-background: #c3f0de;
|
||||
--editor-colors-light-blue-background: #c5eff9;
|
||||
--editor-colors-dark-blue-background: #c9dafb;
|
||||
--editor-colors-purple-background: #e3d8fd;
|
||||
/* end background colors */
|
||||
}
|
||||
/* background colors */
|
||||
[data-theme*="light"] {
|
||||
--editor-colors-gray-background: #d6d6d8;
|
||||
--editor-colors-peach-background: #ffd5d7;
|
||||
--editor-colors-pink-background: #fdd4e3;
|
||||
--editor-colors-orange-background: #ffe3cd;
|
||||
--editor-colors-green-background: #c3f0de;
|
||||
--editor-colors-light-blue-background: #c5eff9;
|
||||
--editor-colors-dark-blue-background: #c9dafb;
|
||||
--editor-colors-purple-background: #e3d8fd;
|
||||
}
|
||||
[data-theme*="dark"] {
|
||||
--editor-colors-gray-background: #404144;
|
||||
--editor-colors-peach-background: #593032;
|
||||
--editor-colors-pink-background: #562e3d;
|
||||
--editor-colors-orange-background: #583e2a;
|
||||
--editor-colors-green-background: #1d4a3b;
|
||||
--editor-colors-light-blue-background: #1f495c;
|
||||
--editor-colors-dark-blue-background: #223558;
|
||||
--editor-colors-purple-background: #3d325a;
|
||||
}
|
||||
/* end background colors */
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -355,7 +402,6 @@ body {
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
|
||||
@@ -5,28 +5,41 @@ import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// plane package imports
|
||||
import { ANALYTICS_TABS } from "@plane/constants";
|
||||
import { ANALYTICS_TABS, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
|
||||
// hooks
|
||||
import { useCommandPalette, useEventTracker, useProject, useWorkspace } from "@/hooks/store";
|
||||
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
// plane web
|
||||
import { EUserPermissions } from "@/plane-web/constants";
|
||||
|
||||
const AnalyticsPage = observer(() => {
|
||||
const searchParams = useSearchParams();
|
||||
const analytics_tab = searchParams.get("analytics_tab");
|
||||
// plane imports
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { toggleCreateProjectModal } = useCommandPalette();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { workspaceProjectIds, loader } = useProject();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// helper hooks
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" });
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined;
|
||||
|
||||
// permissions
|
||||
const canPerformEmptyStateActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
// TODO: refactor loader implementation
|
||||
return (
|
||||
<>
|
||||
@@ -67,12 +80,22 @@ const AnalyticsPage = observer(() => {
|
||||
</Tab.Group>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
type={EmptyStateType.WORKSPACE_ANALYTICS}
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Analytics empty state");
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
<DetailedEmptyState
|
||||
title={t("workspace_analytics.empty_state.general.title")}
|
||||
description={t("workspace_analytics.empty_state.general.description")}
|
||||
assetPath={resolvedPath}
|
||||
customPrimaryButton={
|
||||
<ComicBoxButton
|
||||
label={t("workspace_analytics.empty_state.general.primary_button.text")}
|
||||
title={t("workspace_analytics.empty_state.general.primary_button.comic.title")}
|
||||
description={t("workspace_analytics.empty_state.general.primary_button.comic.description")}
|
||||
onClick={() => {
|
||||
setTrackElement("Analytics empty state");
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
disabled={!canPerformEmptyStateActions}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Home } from "lucide-react";
|
||||
import githubBlackImage from "/public/logos/github-black.png";
|
||||
import githubWhiteImage from "/public/logos/github-white.png";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
@@ -19,6 +20,7 @@ export const WorkspaceDashboardHeader = () => {
|
||||
// hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -28,7 +30,9 @@ export const WorkspaceDashboardHeader = () => {
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={<BreadcrumbLink label="Home" icon={<Home className="h-4 w-4 text-custom-text-300" />} />}
|
||||
link={
|
||||
<BreadcrumbLink label={t("home.title")} icon={<Home className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
@@ -51,7 +55,7 @@ export const WorkspaceDashboardHeader = () => {
|
||||
width={16}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
<span className="hidden text-xs font-medium sm:hidden md:block">Star us on GitHub</span>
|
||||
<span className="hidden text-xs font-medium sm:hidden md:block">{t("home.star_us_on_github")}</span>
|
||||
</a>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
|
||||
@@ -4,21 +4,24 @@ import { useCallback, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
import { InboxContentRoot } from "@/components/inbox";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
|
||||
// hooks
|
||||
import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
|
||||
|
||||
const WorkspaceDashboardPage = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const {
|
||||
@@ -34,6 +37,7 @@ const WorkspaceDashboardPage = observer(() => {
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Inbox` : undefined;
|
||||
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
|
||||
notificationLiteByNotificationId(currentSelectedNotificationId);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" });
|
||||
|
||||
// fetching workspace issue properties
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
@@ -82,7 +86,7 @@ const WorkspaceDashboardPage = observer(() => {
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto">
|
||||
{!currentSelectedNotificationId ? (
|
||||
<div className="w-full h-screen flex justify-center items-center">
|
||||
<EmptyState type={EmptyStateType.NOTIFICATION_DETAIL_EMPTY_STATE} layout="screen-simple" />
|
||||
<SimpleEmptyState title={t("notification.empty_state.detail.title")} assetPath={resolvedPath} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PageHead, AppHeader, ContentWrapper } from "@/components/core";
|
||||
import { WorkspaceHomeView } from "@/components/home";
|
||||
// hooks
|
||||
@@ -11,8 +12,9 @@ import { WorkspaceDashboardHeader } from "./header";
|
||||
|
||||
const WorkspaceDashboardPage = observer(() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Home` : undefined;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - ${t("home.title")}` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -6,12 +6,13 @@ import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ChevronDown, PanelRight } from "lucide-react";
|
||||
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IUserProfileProjectSegregation } from "@plane/types";
|
||||
import { Breadcrumbs, Header, CustomMenu, UserActivityIcon } from "@plane/ui";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// components
|
||||
import { ProfileIssuesFilter } from "@/components/profile";
|
||||
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { useAppTheme, useUser, useUserPermissions } from "@/hooks/store";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
@@ -30,6 +31,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||
const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme();
|
||||
const { data: currentUser } = useUser();
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const isAuthorized = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
@@ -44,7 +46,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||
|
||||
const isCurrentUser = currentUser?.id === userId;
|
||||
|
||||
const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Work`;
|
||||
const breadcrumbLabel = isCurrentUser ? t("profile.page_label") : `${userName} ${t("profile.work")}`;
|
||||
|
||||
return (
|
||||
<Header>
|
||||
@@ -86,7 +88,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
|
||||
className="w-full text-custom-text-300"
|
||||
>
|
||||
{tab.label}
|
||||
{t(tab.i18n_label)}
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||
import { ProfileSidebar } from "@/components/profile";
|
||||
// constants
|
||||
import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys";
|
||||
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
@@ -66,7 +66,7 @@ const UseProfileLayout: React.FC<Props> = observer((props) => {
|
||||
<AppHeader
|
||||
header={
|
||||
<UserProfileHeader
|
||||
type={currentTab?.label}
|
||||
type={currentTab?.i18n_label}
|
||||
userProjectsData={userProjectsData}
|
||||
showProfileIssuesFilter={isIssuesTab}
|
||||
/>
|
||||
|
||||
@@ -6,21 +6,30 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssueFilterType,
|
||||
EIssuesStoreType,
|
||||
ISSUE_LAYOUTS,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
} from "@plane/constants";
|
||||
// plane i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
|
||||
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel } from "@/hooks/store";
|
||||
|
||||
export const ProfileIssuesMobileHeader = observer(() => {
|
||||
// plane i18n
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
// store hook
|
||||
@@ -112,7 +121,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
||||
placement="bottom-start"
|
||||
customButton={
|
||||
<div className="flex flex-center text-sm text-custom-text-200">
|
||||
Layout
|
||||
{t("common.layout")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
@@ -129,19 +138,19 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<layout.icon className="h-3 w-3" />
|
||||
<div className="text-custom-text-300">{layout.title}</div>
|
||||
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
|
||||
<div className="text-custom-text-300">{t(layout.i18n_title)}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<div className="flex flex-center text-sm text-custom-text-200">
|
||||
Filters
|
||||
{t("common.filters")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
@@ -149,7 +158,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
||||
>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
|
||||
}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
@@ -163,18 +172,18 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
title={t("common.display")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<div className="flex flex-center text-sm text-custom-text-200">
|
||||
Display
|
||||
{t("common.display")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
||||
@@ -2,12 +2,12 @@ import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
||||
// components
|
||||
// constants
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
|
||||
|
||||
type Props = {
|
||||
isAuthorized: boolean;
|
||||
@@ -33,7 +33,7 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
|
||||
: "border-transparent"
|
||||
}`}
|
||||
>
|
||||
{t(tab.label)}
|
||||
{t(tab.i18n_label)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// types
|
||||
import { GROUP_CHOICES } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IUserStateDistribution, TStateGroups } from "@plane/types";
|
||||
// components
|
||||
import { ContentWrapper } from "@plane/ui";
|
||||
@@ -16,7 +18,6 @@ import {
|
||||
} from "@/components/profile";
|
||||
// constants
|
||||
import { USER_PROFILE_DATA } from "@/constants/fetch-keys";
|
||||
import { GROUP_CHOICES } from "@/constants/project";
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
|
||||
@@ -26,6 +27,7 @@ const userService = new UserService();
|
||||
export default function ProfileOverviewPage() {
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { data: userProfile } = useSWR(
|
||||
workspaceSlug && userId ? USER_PROFILE_DATA(workspaceSlug.toString(), userId.toString()) : null,
|
||||
workspaceSlug && userId ? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString()) : null
|
||||
@@ -40,7 +42,7 @@ export default function ProfileOverviewPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title="Your work" />
|
||||
<PageHead title={t("profile.page_label")} />
|
||||
<ContentWrapper className="space-y-7">
|
||||
<ProfileStats userProfile={userProfile} />
|
||||
<ProfileWorkload stateDistribution={stateDistribution} />
|
||||
|
||||
+12
-10
@@ -7,7 +7,9 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ArrowRight, PanelRight } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
|
||||
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
@@ -16,8 +18,6 @@ import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header } from "
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
@@ -72,6 +72,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
projectId: string;
|
||||
cycleId: string;
|
||||
};
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
@@ -184,7 +186,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
label="Cycles"
|
||||
label={t("common.cycles")}
|
||||
href={`/${workspaceSlug}/projects/${projectId}/cycles`}
|
||||
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
@@ -239,7 +241,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
@@ -247,7 +249,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
@@ -258,10 +260,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
@@ -276,7 +278,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
{canUserCreateIssue && (
|
||||
<>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
{t("common.analytics")}
|
||||
</Button>
|
||||
{!isCompletedCycle && (
|
||||
<Button
|
||||
@@ -287,7 +289,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
Add issue
|
||||
{t("issue.add")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
+29
-17
@@ -5,32 +5,42 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssueFilterType,
|
||||
EIssuesStoreType,
|
||||
ISSUE_LAYOUTS,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
} from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
|
||||
|
||||
export const CycleIssuesMobileHeader = () => {
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { getCycleById } = useCycle();
|
||||
const layouts = [
|
||||
{ key: "list", title: "List", icon: List },
|
||||
{ key: "kanban", title: "Board", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar", icon: Calendar },
|
||||
{ key: "list", titleTranslationKey: "issue.layouts.list", icon: List },
|
||||
{ key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban },
|
||||
{ key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar },
|
||||
];
|
||||
|
||||
const { workspaceSlug, projectId, cycleId } = useParams();
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
@@ -123,7 +133,9 @@ export const CycleIssuesMobileHeader = () => {
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
placement="bottom-start"
|
||||
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
|
||||
customButton={
|
||||
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{t("common.layout")}</span>
|
||||
}
|
||||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
@@ -135,18 +147,18 @@ export const CycleIssuesMobileHeader = () => {
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<layout.icon className="w-3 h-3" />
|
||||
<div className="text-custom-text-300">{layout.title}</div>
|
||||
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="w-3 h-3" />
|
||||
<div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Filters
|
||||
{t("common.filters")}
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
@@ -156,7 +168,7 @@ export const CycleIssuesMobileHeader = () => {
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
@@ -170,18 +182,18 @@ export const CycleIssuesMobileHeader = () => {
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
title={t("common.display")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Display
|
||||
{t("common.display")}
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
@@ -198,7 +210,7 @@ export const CycleIssuesMobileHeader = () => {
|
||||
onClick={() => setAnalyticsModal(true)}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
|
||||
>
|
||||
Analytics
|
||||
{t("common.analytics")}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
||||
+44
-14
@@ -3,20 +3,22 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TCycleFilters } from "@plane/types";
|
||||
// components
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
|
||||
import { CycleModuleListLayout } from "@/components/ui";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useCycle, useProject, useCycleFilter } from "@/hooks/store";
|
||||
import { useEventTracker, useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
const ProjectCyclesPage = observer(() => {
|
||||
// states
|
||||
@@ -26,13 +28,23 @@ const ProjectCyclesPage = observer(() => {
|
||||
const { currentProjectCycleIds, loader } = useCycle();
|
||||
const { getProjectById, currentProjectDetails } = useProject();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// cycle filters hook
|
||||
const { clearAllFilters, currentProjectFilters, updateFilters } = useCycleFilter();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
||||
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
|
||||
const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
const hasMemberLevelPermission = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/cycles" });
|
||||
|
||||
const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => {
|
||||
if (!projectId) return;
|
||||
@@ -50,9 +62,17 @@ const ProjectCyclesPage = observer(() => {
|
||||
if (currentProjectDetails?.cycle_view === false)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.DISABLED_PROJECT_CYCLE}
|
||||
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
|
||||
<DetailedEmptyState
|
||||
title={t("disabled_project.empty_state.cycle.title")}
|
||||
description={t("disabled_project.empty_state.cycle.description")}
|
||||
assetPath={resolvedPath}
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.cycle.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
},
|
||||
disabled: !hasAdminLevelPermission,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -71,12 +91,22 @@ const ProjectCyclesPage = observer(() => {
|
||||
/>
|
||||
{totalCycles === 0 ? (
|
||||
<div className="h-full place-items-center">
|
||||
<EmptyState
|
||||
type={EmptyStateType.PROJECT_CYCLES}
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Cycle empty state");
|
||||
setCreateModal(true);
|
||||
}}
|
||||
<DetailedEmptyState
|
||||
title={t("project_cycles.empty_state.general.title")}
|
||||
description={t("project_cycles.empty_state.general.description")}
|
||||
assetPath={resolvedPath}
|
||||
customPrimaryButton={
|
||||
<ComicBoxButton
|
||||
label={t("project_cycles.empty_state.general.primary_button.text")}
|
||||
title={t("project_cycles.empty_state.general.primary_button.comic.title")}
|
||||
description={t("project_cycles.empty_state.general.primary_button.comic.description")}
|
||||
onClick={() => {
|
||||
setTrackElement("Cycle empty state");
|
||||
setCreateModal(true);
|
||||
}}
|
||||
disabled={!hasMemberLevelPermission}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
+13
-7
@@ -4,7 +4,9 @@ import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
|
||||
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
@@ -12,8 +14,6 @@ import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
@@ -23,6 +23,8 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
|
||||
export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
|
||||
// store hooks
|
||||
@@ -122,14 +124,18 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
@@ -138,10 +144,10 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
||||
@@ -2,35 +2,50 @@
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { EUserProjectRoles } from "@plane/constants/src/user";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { InboxIssueRoot } from "@/components/inbox";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// helpers
|
||||
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
const ProjectInboxPage = observer(() => {
|
||||
/// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const navigationTab = searchParams.get("currentTab");
|
||||
const inboxIssueId = searchParams.get("inboxIssueId");
|
||||
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/intake" });
|
||||
|
||||
// No access to inbox
|
||||
if (currentProjectDetails?.inbox_view === false)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.DISABLED_PROJECT_INBOX}
|
||||
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
|
||||
<DetailedEmptyState
|
||||
title={t("disabled_project.empty_state.inbox.title")}
|
||||
description={t("disabled_project.empty_state.inbox.description")}
|
||||
assetPath={resolvedPath}
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.inbox.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+30
-17
@@ -6,26 +6,39 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssueFilterType,
|
||||
EIssuesStoreType,
|
||||
ISSUE_LAYOUTS,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
} from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FilterSelection,
|
||||
FiltersDropdown,
|
||||
IssueLayoutIcon,
|
||||
} from "@/components/issues/issue-layouts";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
export const ProjectIssuesMobileHeader = observer(() => {
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
const layouts = [
|
||||
{ key: "list", title: "List", icon: List },
|
||||
{ key: "kanban", title: "Board", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar", icon: Calendar },
|
||||
{ key: "list", titleTranslationKey: "issue.layouts.list", icon: List },
|
||||
{ key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban },
|
||||
{ key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar },
|
||||
];
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { workspaceSlug, projectId } = useParams() as {
|
||||
@@ -104,7 +117,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
placement="bottom-start"
|
||||
customButton={
|
||||
<div className="flex flex-start text-sm text-custom-text-200">
|
||||
Layout
|
||||
{t("common.layout")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
@@ -119,18 +132,18 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<layout.icon className="h-3 w-3" />
|
||||
<div className="text-custom-text-300">{layout.title}</div>
|
||||
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
|
||||
<div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-sm text-custom-text-200">
|
||||
Filters
|
||||
{t("common.filters")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
@@ -142,7 +155,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
@@ -154,18 +167,18 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
title={t("common.display")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-sm text-custom-text-200">
|
||||
Display
|
||||
{t("common.display")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
@@ -181,7 +194,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
onClick={() => setAnalyticsModal(true)}
|
||||
className="flex flex-grow justify-center border-l border-custom-border-200 text-sm text-custom-text-200"
|
||||
>
|
||||
Analytics
|
||||
{t("common.analytics")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
+3
-5
@@ -7,7 +7,7 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ArrowRight, PanelRight } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, EIssuesStoreType, EIssueFilterType } from "@plane/constants";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
@@ -16,8 +16,6 @@ import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header } from "@pla
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
@@ -247,7 +245,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
@@ -259,7 +257,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
||||
+23
-11
@@ -6,16 +6,27 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssueFilterType,
|
||||
EIssuesStoreType,
|
||||
ISSUE_LAYOUTS,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
} from "@plane/constants";
|
||||
// plane i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FilterSelection,
|
||||
FiltersDropdown,
|
||||
IssueLayoutIcon,
|
||||
} from "@/components/issues/issue-layouts";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
@@ -25,10 +36,11 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getModuleById } = useModule();
|
||||
const { t } = useTranslation();
|
||||
const layouts = [
|
||||
{ key: "list", title: "List", icon: List },
|
||||
{ key: "kanban", title: "Board", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar", icon: Calendar },
|
||||
{ key: "list", i18n_title: "issue.layouts.list", icon: List },
|
||||
{ key: "kanban", i18n_title: "issue.layouts.kanban", icon: Kanban },
|
||||
{ key: "calendar", i18n_title: "issue.layouts.calendar", icon: Calendar },
|
||||
];
|
||||
const { workspaceSlug, projectId, moduleId } = useParams() as {
|
||||
workspaceSlug: string;
|
||||
@@ -116,8 +128,8 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<layout.icon className="h-3 w-3" />
|
||||
<div className="text-custom-text-300">{layout.title}</div>
|
||||
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
|
||||
<div className="text-custom-text-300">{t(layout.i18n_title)}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
@@ -139,7 +151,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
@@ -162,7 +174,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
||||
+24
-7
@@ -4,27 +4,36 @@ import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TModuleFilters } from "@plane/types";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useModuleFilter, useProject } from "@/hooks/store";
|
||||
import { useModuleFilter, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
const ProjectModulesPage = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store
|
||||
const { getProjectById, currentProjectDetails } = useProject();
|
||||
const { currentProjectFilters, currentProjectDisplayFilters, clearAllFilters, updateFilters, updateDisplayFilters } =
|
||||
useModuleFilter();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Modules` : undefined;
|
||||
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/modules" });
|
||||
|
||||
const handleRemoveFilter = useCallback(
|
||||
(key: keyof TModuleFilters, value: string | null) => {
|
||||
@@ -45,9 +54,17 @@ const ProjectModulesPage = observer(() => {
|
||||
if (currentProjectDetails?.module_view === false)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.DISABLED_PROJECT_MODULE}
|
||||
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
|
||||
<DetailedEmptyState
|
||||
title={t("disabled_project.empty_state.module.title")}
|
||||
description={t("disabled_project.empty_state.module.description")}
|
||||
assetPath={resolvedPath}
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.module.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+24
-8
@@ -2,27 +2,35 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
// types
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TPageNavigationTabs } from "@plane/types";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { PagesListRoot, PagesListView } from "@/components/pages";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
const ProjectPagesPage = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const type = searchParams.get("type");
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { getProjectById, currentProjectDetails } = useProject();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
|
||||
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/pages" });
|
||||
|
||||
const currentPageType = (): TPageNavigationTabs => {
|
||||
const pageType = type?.toString();
|
||||
@@ -37,9 +45,17 @@ const ProjectPagesPage = observer(() => {
|
||||
if (currentProjectDetails?.page_view === false)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.DISABLED_PROJECT_PAGE}
|
||||
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
|
||||
<DetailedEmptyState
|
||||
title={t("disabled_project.empty_state.page.title")}
|
||||
description={t("disabled_project.empty_state.page.description")}
|
||||
assetPath={resolvedPath}
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.page.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -43,8 +43,6 @@ const GeneralSettingsPage = observer(() => {
|
||||
);
|
||||
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
|
||||
// const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
|
||||
// const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+24
-21
@@ -1,12 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Layers, Lock } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssueFilterType,
|
||||
EIssuesStoreType,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
EViewAccess,
|
||||
} from "@plane/constants";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
@@ -15,11 +21,9 @@ import { Breadcrumbs, Button, CustomMenu, Tooltip, Header } from "@plane/ui";
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
import { ViewQuickActions } from "@/components/views";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { getPublishViewLink } from "@/helpers/project-views.helpers";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
@@ -38,6 +42,8 @@ import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
// refs
|
||||
const parentRef = useRef(null);
|
||||
// router
|
||||
const { workspaceSlug, projectId, viewId } = useParams();
|
||||
// store hooks
|
||||
@@ -133,7 +139,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
const publishLink = getPublishViewLink(viewDetails?.anchor);
|
||||
|
||||
if (!viewDetails) return;
|
||||
|
||||
return (
|
||||
<Header>
|
||||
@@ -203,19 +210,14 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
<></>
|
||||
)}
|
||||
|
||||
{viewDetails?.anchor && publishLink ? (
|
||||
<a
|
||||
href={publishLink}
|
||||
className="px-3 py-1.5 bg-green-500/20 text-green-500 rounded text-xs font-medium flex items-center gap-1.5"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="flex-shrink-0 rounded-full size-1.5 bg-green-500" />
|
||||
Live
|
||||
</a>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div className="hidden md:block">
|
||||
<ViewQuickActions
|
||||
parentRef={parentRef}
|
||||
projectId={projectId.toString()}
|
||||
view={viewDetails}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
{!viewDetails?.is_locked ? (
|
||||
@@ -244,8 +246,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
projectId={projectId.toString()}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
@@ -256,7 +259,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
||||
+23
-7
@@ -4,29 +4,37 @@ import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { EUserPermissionsLevel, EUserProjectRoles, EViewAccess } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TViewFilterProps } from "@plane/types";
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { ProjectViewsList } from "@/components/views";
|
||||
import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// constants
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useProject, useProjectView } from "@/hooks/store";
|
||||
import { useProject, useProjectView, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
const ProjectViewsPage = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store
|
||||
const { getProjectById, currentProjectDetails } = useProject();
|
||||
const { filters, updateFilters, clearAllFilters } = useProjectView();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Views` : undefined;
|
||||
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/views" });
|
||||
|
||||
const handleRemoveFilter = useCallback(
|
||||
(key: keyof TViewFilterProps, value: string | EViewAccess | null) => {
|
||||
@@ -53,9 +61,17 @@ const ProjectViewsPage = observer(() => {
|
||||
if (currentProjectDetails?.issue_views_view === false)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.DISABLED_PROJECT_VIEW}
|
||||
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
|
||||
<DetailedEmptyState
|
||||
title={t("disabled_project.empty_state.view.title")}
|
||||
description={t("disabled_project.empty_state.view.description")}
|
||||
assetPath={resolvedPath}
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.view.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user