Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 035d32bf94 | |||
| 348e8af8ab | |||
| f00d6e10ce | |||
| 8f3a0be177 | |||
| 0679e140a2 | |||
| b611f5110f | |||
| 0f7bc6979f | |||
| 12501d0597 | |||
| 3a86fff7c1 | |||
| 58a4b45463 |
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -667,3 +669,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,
|
||||
|
||||
@@ -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)
|
||||
@@ -316,5 +316,104 @@
|
||||
"change_parent_issue": "Change parent issue",
|
||||
"remove_parent_issue": "Remove parent issue",
|
||||
"add_parent": "Add parent",
|
||||
"loading_members": "Loading members..."
|
||||
"loading_members": "Loading members...",
|
||||
|
||||
"date_time" : {
|
||||
"units_short": {
|
||||
"s": "s",
|
||||
"m": "min",
|
||||
"h": "h",
|
||||
"d": "j",
|
||||
"w": "sem",
|
||||
"mo": "mois",
|
||||
"y": "an"
|
||||
},
|
||||
|
||||
"date": {
|
||||
"days": {
|
||||
"Monday": "Monday",
|
||||
"Tuesday": "Tuesday",
|
||||
"Wednesday": "Wednesday",
|
||||
"Thursday": "Thursday",
|
||||
"Friday": "Friday",
|
||||
"Saturday": "Saturday",
|
||||
"Sunday": "Sunday"
|
||||
},
|
||||
"months_short": {
|
||||
"Jan": "Jan",
|
||||
"Feb": "Feb",
|
||||
"Mar": "Mar",
|
||||
"Apr": "Apr",
|
||||
"May": "May",
|
||||
"Jun": "Jun",
|
||||
"Jul": "Jul",
|
||||
"Aug": "Aug",
|
||||
"Sep": "Sep",
|
||||
"Oct": "Oct",
|
||||
"Nov": "Nov",
|
||||
"Dec": "Dec"
|
||||
},
|
||||
"format": {
|
||||
"full": "{day}, {month} {date} {time}",
|
||||
"shortDate": "{month} {date}",
|
||||
"timeOnly": "{hours}:{minutes}"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"less_than_x_seconds": {
|
||||
"past": "{time, plural, one{less than # second} other{less than # seconds}} ago",
|
||||
"future": "in {time, plural, one{less than # second} other{less than # seconds}}"
|
||||
},
|
||||
"less_than_x_minutes": {
|
||||
"past": "less than a minute ago",
|
||||
"future": "in less than a minute"
|
||||
},
|
||||
"x_minutes": {
|
||||
"past": "{time, plural, one{# minute} other{# minutes}} ago",
|
||||
"future": "in {time, plural, one{# minute} other{# minutes}}"
|
||||
},
|
||||
"about_x_hours": {
|
||||
"past": "{time, plural, one{about # hour} other{about # hours}} ago",
|
||||
"future": "in {time, plural, one{about # hour} other{about # hours}}"
|
||||
},
|
||||
"x_days": {
|
||||
"past": "{time, plural, one{# day} other{# days}} ago",
|
||||
"future": "in {time, plural, one{# day} other{# days}}"
|
||||
},
|
||||
"about_x_months": {
|
||||
"past": "{time, plural, one{about # month} other{about # months}} ago",
|
||||
"future": "in {time, plural, one{about # month} other{about # months}}"
|
||||
},
|
||||
"about_x_years": {
|
||||
"past": "{time, plural, one{about # year} other{about # years}} ago",
|
||||
"future": "in {time, plural, one{about # year} other{about # years}}"
|
||||
},
|
||||
"over_x_years": {
|
||||
"past": "{time, plural, one{over # year} other{over # years}} ago",
|
||||
"future": "in {time, plural, one{over # year} other{over # years}}"
|
||||
},
|
||||
"almost_x_years": {
|
||||
"past": "{time, plural, one{almost # year} other{almost # years}} ago",
|
||||
"future": "in {time, plural, one{almost # year} other{almost # years}}"
|
||||
}
|
||||
},
|
||||
"include_seconds": {
|
||||
"less_than_5_seconds": {
|
||||
"past": "less than 5 seconds ago",
|
||||
"future": "in less than 5 seconds"
|
||||
},
|
||||
"less_than_10_seconds": {
|
||||
"past": "less than 10 seconds ago",
|
||||
"future": "in less than 10 seconds"
|
||||
},
|
||||
"less_than_20_seconds": {
|
||||
"past": "less than 20 seconds ago",
|
||||
"future": "in less than 20 seconds"
|
||||
},
|
||||
"half_a_minute": {
|
||||
"past": "half a minute ago",
|
||||
"future": "in half a minute"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,5 +316,102 @@
|
||||
"remove_parent_issue": "Eliminar problema padre",
|
||||
"add_parent": "Agregar padre",
|
||||
"loading_members": "Cargando miembros...",
|
||||
"inbox": "bandeja de entrada"
|
||||
"inbox": "bandeja de entrada",
|
||||
"date_time": {
|
||||
"units_short": {
|
||||
"s": "s",
|
||||
"m": "min",
|
||||
"h": "h",
|
||||
"d": "d",
|
||||
"w": "sem",
|
||||
"mo": "mes",
|
||||
"y": "año"
|
||||
},
|
||||
"date": {
|
||||
"days": {
|
||||
"Monday": "lunes",
|
||||
"Tuesday": "martes",
|
||||
"Wednesday": "miércoles",
|
||||
"Thursday": "jueves",
|
||||
"Friday": "viernes",
|
||||
"Saturday": "sábado",
|
||||
"Sunday": "domingo"
|
||||
},
|
||||
"months_short": {
|
||||
"Jan": "ene.",
|
||||
"Feb": "feb.",
|
||||
"Mar": "mar.",
|
||||
"Apr": "abr.",
|
||||
"May": "may.",
|
||||
"Jun": "jun.",
|
||||
"Jul": "jul.",
|
||||
"Aug": "ago.",
|
||||
"Sep": "sept.",
|
||||
"Oct": "oct.",
|
||||
"Nov": "nov.",
|
||||
"Dec": "dic."
|
||||
},
|
||||
"format": {
|
||||
"full": "{day} {date} de {month} {time}",
|
||||
"shortDate": "{date} {month}",
|
||||
"timeOnly": "{hours}:{minutes}"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"less_than_x_seconds": {
|
||||
"past": "hace {time, plural, one{menos de un segundo} other{menos de # segundos}}",
|
||||
"future": "en {time, plural, one{menos de un segundo} other{menos de # segundos}}"
|
||||
},
|
||||
"less_than_x_minutes": {
|
||||
"past": "hace menos de un minuto",
|
||||
"future": "en menos de un minuto"
|
||||
},
|
||||
"x_minutes": {
|
||||
"past": "hace {time, plural, one{# minuto} other{# minutos}}",
|
||||
"future": "en {time, plural, one{# minuto} other{# minutos}}"
|
||||
},
|
||||
"about_x_hours": {
|
||||
"past": "hace {time, plural, one{aproximadamente # hora} other{aproximadamente # horas}}",
|
||||
"future": "en {time, plural, one{aproximadamente # hora} other{aproximadamente # horas}}"
|
||||
},
|
||||
"x_days": {
|
||||
"past": "hace {time, plural, one{# día} other{# días}}",
|
||||
"future": "en {time, plural, one{# día} other{# días}}"
|
||||
},
|
||||
"about_x_months": {
|
||||
"past": "hace {time, plural, one{aproximadamente # mes} other{aproximadamente # meses}}",
|
||||
"future": "en {time, plural, one{aproximadamente # mes} other{aproximadamente # meses}}"
|
||||
},
|
||||
"about_x_years": {
|
||||
"past": "hace {time, plural, one{aproximadamente # año} other{aproximadamente # años}}",
|
||||
"future": "en {time, plural, one{aproximadamente # año} other{aproximadamente # años}}"
|
||||
},
|
||||
"over_x_years": {
|
||||
"past": "hace {time, plural, one{más de # año} other{más de # años}}",
|
||||
"future": "en {time, plural, one{más de # año} other{más de # años}}"
|
||||
},
|
||||
"almost_x_years": {
|
||||
"past": "hace {time, plural, one{casi # año} other{casi # años}}",
|
||||
"future": "en {time, plural, one{casi # año} other{casi # años}}"
|
||||
}
|
||||
},
|
||||
"include_seconds": {
|
||||
"less_than_5_seconds": {
|
||||
"past": "hace menos de 5 segundos",
|
||||
"future": "en menos de 5 segundos"
|
||||
},
|
||||
"less_than_10_seconds": {
|
||||
"past": "hace menos de 10 segundos",
|
||||
"future": "en menos de 10 segundos"
|
||||
},
|
||||
"less_than_20_seconds": {
|
||||
"past": "hace menos de 20 segundos",
|
||||
"future": "en menos de 20 segundos"
|
||||
},
|
||||
"half_a_minute": {
|
||||
"past": "hace medio minuto",
|
||||
"future": "en medio minuto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,5 +316,102 @@
|
||||
"remove_parent_issue": "Supprimer le problème parent",
|
||||
"add_parent": "Ajouter un parent",
|
||||
"loading_members": "Chargement des membres...",
|
||||
"inbox": "Boîte de réception"
|
||||
"inbox": "Boîte de réception",
|
||||
"date_time": {
|
||||
"units_short": {
|
||||
"s": "s",
|
||||
"m": "min",
|
||||
"h": "h",
|
||||
"d": "j",
|
||||
"w": "sem",
|
||||
"mo": "mois",
|
||||
"y": "an"
|
||||
},
|
||||
"date": {
|
||||
"days": {
|
||||
"Monday": "lundi",
|
||||
"Tuesday": "mardi",
|
||||
"Wednesday": "mercredi",
|
||||
"Thursday": "jeudi",
|
||||
"Friday": "vendredi",
|
||||
"Saturday": "samedi",
|
||||
"Sunday": "dimanche"
|
||||
},
|
||||
"months_short": {
|
||||
"Jan": "janv.",
|
||||
"Feb": "févr.",
|
||||
"Mar": "mars",
|
||||
"Apr": "avr.",
|
||||
"May": "mai",
|
||||
"Jun": "juin",
|
||||
"Jul": "juil.",
|
||||
"Aug": "août",
|
||||
"Sep": "sept.",
|
||||
"Oct": "oct.",
|
||||
"Nov": "nov.",
|
||||
"Dec": "déc."
|
||||
},
|
||||
"format": {
|
||||
"full": "{day} {date} {month} {time}",
|
||||
"shortDate": "{date} {month}",
|
||||
"timeOnly": "{hours}:{minutes}"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"less_than_x_seconds": {
|
||||
"past": "il y a {time, plural, one{moins d'une seconde} other{moins de # secondes}}",
|
||||
"future": "dans {time, plural, one{moins d'une seconde} other{moins de # secondes}}"
|
||||
},
|
||||
"less_than_x_minutes": {
|
||||
"past": "il y a moins d'une minute",
|
||||
"future": "dans moins d'une minute"
|
||||
},
|
||||
"x_minutes": {
|
||||
"past": "il y a {time, plural, one{# minute} other{# minutes}}",
|
||||
"future": "dans {time, plural, one{# minute} other{# minutes}}"
|
||||
},
|
||||
"about_x_hours": {
|
||||
"past": "il y a {time, plural, one{environ # heure} other{environ # heures}}",
|
||||
"future": "dans {time, plural, one{environ # heure} other{environ # heures}}"
|
||||
},
|
||||
"x_days": {
|
||||
"past": "il y a {time, plural, one{# jour} other{# jours}}",
|
||||
"future": "dans {time, plural, one{# jour} other{# jours}}"
|
||||
},
|
||||
"about_x_months": {
|
||||
"past": "il y a {time, plural, one{environ # mois} other{environ # mois}}",
|
||||
"future": "dans {time, plural, one{environ # mois} other{environ # mois}}"
|
||||
},
|
||||
"about_x_years": {
|
||||
"past": "il y a {time, plural, one{environ # an} other{environ # ans}}",
|
||||
"future": "dans {time, plural, one{environ # an} other{environ # ans}}"
|
||||
},
|
||||
"over_x_years": {
|
||||
"past": "il y a {time, plural, one{plus d'un an} other{plus de # ans}}",
|
||||
"future": "dans {time, plural, one{plus d'un an} other{plus de # ans}}"
|
||||
},
|
||||
"almost_x_years": {
|
||||
"past": "il y a {time, plural, one{presque # an} other{presque # ans}}",
|
||||
"future": "dans {time, plural, one{presque # an} other{presque # ans}}"
|
||||
}
|
||||
},
|
||||
"include_seconds": {
|
||||
"less_than_5_seconds": {
|
||||
"past": "il y a moins de 5 secondes",
|
||||
"future": "dans moins de 5 secondes"
|
||||
},
|
||||
"less_than_10_seconds": {
|
||||
"past": "il y a moins de 10 secondes",
|
||||
"future": "dans moins de 10 secondes"
|
||||
},
|
||||
"less_than_20_seconds": {
|
||||
"past": "il y a moins de 20 secondes",
|
||||
"future": "dans moins de 20 secondes"
|
||||
},
|
||||
"half_a_minute": {
|
||||
"past": "il y a 30 secondes",
|
||||
"future": "dans 30 secondes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,5 +316,102 @@
|
||||
"remove_parent_issue": "親問題を削除",
|
||||
"add_parent": "親問題を追加",
|
||||
"loading_members": "メンバーを読み込んでいます...",
|
||||
"inbox": "受信箱"
|
||||
"inbox": "受信箱",
|
||||
"date_time": {
|
||||
"units_short": {
|
||||
"s": "秒",
|
||||
"m": "分",
|
||||
"h": "時間",
|
||||
"d": "日",
|
||||
"w": "週間",
|
||||
"mo": "ヶ月",
|
||||
"y": "年"
|
||||
},
|
||||
"date": {
|
||||
"days": {
|
||||
"Monday": "月曜日",
|
||||
"Tuesday": "火曜日",
|
||||
"Wednesday": "水曜日",
|
||||
"Thursday": "木曜日",
|
||||
"Friday": "金曜日",
|
||||
"Saturday": "土曜日",
|
||||
"Sunday": "日曜日"
|
||||
},
|
||||
"months_short": {
|
||||
"Jan": "1月",
|
||||
"Feb": "2月",
|
||||
"Mar": "3月",
|
||||
"Apr": "4月",
|
||||
"May": "5月",
|
||||
"Jun": "6月",
|
||||
"Jul": "7月",
|
||||
"Aug": "8月",
|
||||
"Sep": "9月",
|
||||
"Oct": "10月",
|
||||
"Nov": "11月",
|
||||
"Dec": "12月"
|
||||
},
|
||||
"format": {
|
||||
"full": "{month}{date}日 ({day}) {time}",
|
||||
"shortDate": "{month}{date}日",
|
||||
"timeOnly": "{hours}:{minutes}"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"less_than_x_seconds": {
|
||||
"past": "{time}秒未満前",
|
||||
"future": "{time}秒未満後"
|
||||
},
|
||||
"less_than_x_minutes": {
|
||||
"past": "1分未満前",
|
||||
"future": "1分未満後"
|
||||
},
|
||||
"x_minutes": {
|
||||
"past": "{time}分前",
|
||||
"future": "{time}分後"
|
||||
},
|
||||
"about_x_hours": {
|
||||
"past": "約{time}時間前",
|
||||
"future": "約{time}時間後"
|
||||
},
|
||||
"x_days": {
|
||||
"past": "{time}日前",
|
||||
"future": "{time}日後"
|
||||
},
|
||||
"about_x_months": {
|
||||
"past": "約{time}ヶ月前",
|
||||
"future": "約{time}ヶ月後"
|
||||
},
|
||||
"about_x_years": {
|
||||
"past": "約{time}年前",
|
||||
"future": "約{time}年後"
|
||||
},
|
||||
"over_x_years": {
|
||||
"past": "{time}年以上前",
|
||||
"future": "{time}年以上後"
|
||||
},
|
||||
"almost_x_years": {
|
||||
"past": "ほぼ{time}年前",
|
||||
"future": "ほぼ{time}年後"
|
||||
}
|
||||
},
|
||||
"include_seconds": {
|
||||
"less_than_5_seconds": {
|
||||
"past": "5秒未満前",
|
||||
"future": "5秒未満後"
|
||||
},
|
||||
"less_than_10_seconds": {
|
||||
"past": "10秒未満前",
|
||||
"future": "10秒未満後"
|
||||
},
|
||||
"less_than_20_seconds": {
|
||||
"past": "20秒未満前",
|
||||
"future": "20秒未満後"
|
||||
},
|
||||
"half_a_minute": {
|
||||
"past": "30秒前",
|
||||
"future": "30秒後"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,5 +315,102 @@
|
||||
"remove_parent_issue": "移除父问题",
|
||||
"add_parent": "添加父问题",
|
||||
"loading_members": "正在加载成员...",
|
||||
"inbox": "收件箱"
|
||||
"inbox": "收件箱",
|
||||
"date_time": {
|
||||
"units_short": {
|
||||
"s": "秒",
|
||||
"m": "分",
|
||||
"h": "小时",
|
||||
"d": "天",
|
||||
"w": "周",
|
||||
"mo": "月",
|
||||
"y": "年"
|
||||
},
|
||||
"date": {
|
||||
"days": {
|
||||
"Monday": "星期一",
|
||||
"Tuesday": "星期二",
|
||||
"Wednesday": "星期三",
|
||||
"Thursday": "星期四",
|
||||
"Friday": "星期五",
|
||||
"Saturday": "星期六",
|
||||
"Sunday": "星期日"
|
||||
},
|
||||
"months_short": {
|
||||
"Jan": "1月",
|
||||
"Feb": "2月",
|
||||
"Mar": "3月",
|
||||
"Apr": "4月",
|
||||
"May": "5月",
|
||||
"Jun": "6月",
|
||||
"Jul": "7月",
|
||||
"Aug": "8月",
|
||||
"Sep": "9月",
|
||||
"Oct": "10月",
|
||||
"Nov": "11月",
|
||||
"Dec": "12月"
|
||||
},
|
||||
"format": {
|
||||
"full": "{month}{date}日 {day} {time}",
|
||||
"shortDate": "{month}{date}日",
|
||||
"timeOnly": "{hours}:{minutes}"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"less_than_x_seconds": {
|
||||
"past": "{time, plural, one{不到1秒} other{不到#秒}}前",
|
||||
"future": "{time, plural, one{不到1秒} other{不到#秒}}后"
|
||||
},
|
||||
"less_than_x_minutes": {
|
||||
"past": "不到1分钟前",
|
||||
"future": "不到1分钟后"
|
||||
},
|
||||
"x_minutes": {
|
||||
"past": "{time, plural, one{#分钟} other{#分钟}}前",
|
||||
"future": "{time, plural, one{#分钟} other{#分钟}}后"
|
||||
},
|
||||
"about_x_hours": {
|
||||
"past": "大约{time, plural, one{#小时} other{#小时}}前",
|
||||
"future": "大约{time, plural, one{#小时} other{#小时}}后"
|
||||
},
|
||||
"x_days": {
|
||||
"past": "{time, plural, one{#天} other{#天}}前",
|
||||
"future": "{time, plural, one{#天} other{#天}}后"
|
||||
},
|
||||
"about_x_months": {
|
||||
"past": "大约{time, plural, one{#个月} other{#个月}}前",
|
||||
"future": "大约{time, plural, one{#个月} other{#个月}}后"
|
||||
},
|
||||
"about_x_years": {
|
||||
"past": "大约{time, plural, one{#年} other{#年}}前",
|
||||
"future": "大约{time, plural, one{#年} other{#年}}后"
|
||||
},
|
||||
"over_x_years": {
|
||||
"past": "超过{time, plural, one{#年} other{#年}}前",
|
||||
"future": "超过{time, plural, one{#年} other{#年}}后"
|
||||
},
|
||||
"almost_x_years": {
|
||||
"past": "接近{time, plural, one{#年} other{#年}}前",
|
||||
"future": "接近{time, plural, one{#年} other{#年}}后"
|
||||
}
|
||||
},
|
||||
"include_seconds": {
|
||||
"less_than_5_seconds": {
|
||||
"past": "不到5秒前",
|
||||
"future": "不到5秒后"
|
||||
},
|
||||
"less_than_10_seconds": {
|
||||
"past": "不到10秒前",
|
||||
"future": "不到10秒后"
|
||||
},
|
||||
"less_than_20_seconds": {
|
||||
"past": "不到20秒前",
|
||||
"future": "不到20秒后"
|
||||
},
|
||||
"half_a_minute": {
|
||||
"past": "半分钟前",
|
||||
"future": "半分钟后"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { XCircle } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IApiToken } from "@plane/types";
|
||||
// components
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { DeleteApiTokenModal } from "@/components/api-token";
|
||||
import { renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { renderFormattedDate, calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// ui
|
||||
// helpers
|
||||
@@ -22,6 +23,9 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(token.expired_at);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -54,7 +58,7 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
|
||||
? token.expired_at
|
||||
? `Expires ${renderFormattedDate(token.expired_at!)}`
|
||||
: "Never expires"
|
||||
: `Expired ${calculateTimeAgo(token.expired_at)}`}
|
||||
: `Expired ${t(i18n_time_ago, { time })}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
import { Network } from "lucide-react";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TWorkspaceBaseActivity } from "@plane/types";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { renderFormattedTime, renderFormattedDate, calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local components
|
||||
@@ -25,8 +26,9 @@ export const ActivityBlockComponent: FC<TActivityBlockComponent> = (props) => {
|
||||
const { icon, activity, ends, children, customUserName } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const { t } = useTranslation();
|
||||
if (!activity) return <></>;
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(activity.created_at);
|
||||
return (
|
||||
<div
|
||||
className={`relative flex items-center gap-2 text-xs ${
|
||||
@@ -44,7 +46,7 @@ export const ActivityBlockComponent: FC<TActivityBlockComponent> = (props) => {
|
||||
tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`}
|
||||
>
|
||||
<span className="whitespace-nowrap text-custom-text-350 font-medium cursor-help">
|
||||
{calculateTimeAgo(activity.created_at)}
|
||||
{t(i18n_time_ago, { time })}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { History } from "lucide-react";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TRecentActivityWidgetResponse } from "@plane/types";
|
||||
// components
|
||||
import { Card, Avatar, getButtonStyling } from "@plane/ui";
|
||||
@@ -12,7 +13,7 @@ import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
|
||||
import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "@/components/dashboard/widgets";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useDashboard, useUser } from "@/hooks/store";
|
||||
@@ -23,6 +24,7 @@ export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
const { dashboardId, workspaceSlug } = props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const { fetchWidgetStats, getWidgetStats } = useDashboard();
|
||||
const widgetStats = getWidgetStats<TRecentActivityWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||
@@ -44,51 +46,52 @@ export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
</Link>
|
||||
{widgetStats.length > 0 ? (
|
||||
<div className="mt-4 space-y-6">
|
||||
{widgetStats.map((activity) => (
|
||||
<div key={activity.id} className="flex gap-5">
|
||||
<div className="flex-shrink-0">
|
||||
{activity.field ? (
|
||||
activity.new_value === "restore" ? (
|
||||
<History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
) : (
|
||||
<div className="flex h-6 w-6 justify-center">
|
||||
<ActivityIcon activity={activity} />
|
||||
</div>
|
||||
)
|
||||
) : activity.actor_detail.avatar_url && activity.actor_detail.avatar_url !== "" ? (
|
||||
<Avatar
|
||||
src={getFileURL(activity.actor_detail.avatar_url)}
|
||||
name={activity.actor_detail.display_name}
|
||||
size={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white">
|
||||
{activity.actor_detail.is_bot
|
||||
? activity.actor_detail.first_name.charAt(0)
|
||||
: activity.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mt-2 break-words">
|
||||
<p className="inline text-sm text-custom-text-200">
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "}
|
||||
</span>
|
||||
{widgetStats.map((activity) => {
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(activity.created_at);
|
||||
return (
|
||||
<div key={activity.id} className="flex gap-5">
|
||||
<div className="flex-shrink-0">
|
||||
{activity.field ? (
|
||||
<ActivityMessage activity={activity} showIssue />
|
||||
activity.new_value === "restore" ? (
|
||||
<History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
) : (
|
||||
<div className="flex h-6 w-6 justify-center">
|
||||
<ActivityIcon activity={activity} />
|
||||
</div>
|
||||
)
|
||||
) : activity.actor_detail.avatar_url && activity.actor_detail.avatar_url !== "" ? (
|
||||
<Avatar
|
||||
src={getFileURL(activity.actor_detail.avatar_url)}
|
||||
name={activity.actor_detail.display_name}
|
||||
size={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span>
|
||||
created <IssueLink activity={activity} />
|
||||
</span>
|
||||
<div className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white">
|
||||
{activity.actor_detail.is_bot
|
||||
? activity.actor_detail.first_name.charAt(0)
|
||||
: activity.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-custom-text-200 whitespace-nowrap">
|
||||
{calculateTimeAgo(activity.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="-mt-2 break-words">
|
||||
<p className="inline text-sm text-custom-text-200">
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "}
|
||||
</span>
|
||||
{activity.field ? (
|
||||
<ActivityMessage activity={activity} showIssue />
|
||||
) : (
|
||||
<span>
|
||||
created <IssueLink activity={activity} />
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-custom-text-200 whitespace-nowrap">{t(i18n_time_ago, { time })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
<Link
|
||||
href={redirectionLink}
|
||||
className={cn(
|
||||
|
||||
@@ -8,7 +8,7 @@ export const STICKY_COLORS_LIST: {
|
||||
{
|
||||
key: "gray",
|
||||
label: "Gray",
|
||||
backgroundColor: "var(--editor-colors-gray-background)",
|
||||
backgroundColor: "rgba(var(--color-background-90))",
|
||||
},
|
||||
{
|
||||
key: "peach",
|
||||
|
||||
@@ -43,7 +43,6 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
||||
showToolbarInitially = true,
|
||||
showToolbar = true,
|
||||
parentClassName = "",
|
||||
placeholder = "Add comment...",
|
||||
uploadFile,
|
||||
...rest
|
||||
} = props;
|
||||
@@ -78,7 +77,6 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
||||
mentionHandler={{
|
||||
renderComponent: () => <></>,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
containerClassName={cn(containerClassName, "relative")}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IUser } from "@plane/types";
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import { useCurrentTime } from "@/hooks/use-current-time";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
||||
export interface IUserGreetingsView {
|
||||
user: IUser;
|
||||
@@ -16,6 +17,7 @@ export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
|
||||
const { user, handleWidgetModal } = props;
|
||||
// current time hook
|
||||
const { currentTime } = useCurrentTime();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const hour = new Intl.DateTimeFormat("en-US", {
|
||||
hour12: false,
|
||||
@@ -23,10 +25,13 @@ export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
|
||||
}).format(currentTime);
|
||||
|
||||
const date = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(currentTime);
|
||||
|
||||
const month = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
}).format(currentTime);
|
||||
|
||||
const weekDay = new Intl.DateTimeFormat("en-US", {
|
||||
weekday: "long",
|
||||
}).format(currentTime);
|
||||
@@ -39,6 +44,12 @@ export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
|
||||
}).format(currentTime);
|
||||
|
||||
const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
|
||||
const translatedTime = t(`date_time.date.format.full`, {
|
||||
day: t(`date_time.date.days.${weekDay}`),
|
||||
date,
|
||||
month: t(`date_time.date.months_short.${month}`),
|
||||
time: timeString,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
@@ -48,9 +59,7 @@ export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
|
||||
</h3>
|
||||
<h6 className="flex items-center gap-2 font-medium text-custom-text-400">
|
||||
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
|
||||
<div>
|
||||
{weekDay}, {date} {timeString}
|
||||
</div>
|
||||
<div>{translatedTime}</div>
|
||||
</h6>
|
||||
</div>
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleWidgetModal} className="my-auto mb-0">
|
||||
|
||||
@@ -5,10 +5,11 @@ import { FC } from "react";
|
||||
// ui
|
||||
import { observer } from "mobx-react";
|
||||
import { Pencil, Trash2, ExternalLink, EllipsisVertical, Link2, Link } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn, copyTextToClipboard } from "@plane/utils";
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo, calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
import { TLinkOperations } from "./use-links";
|
||||
|
||||
@@ -24,6 +25,7 @@ export const ProjectLinkDetail: FC<TProjectLinkDetail> = observer((props) => {
|
||||
const {
|
||||
quickLinks: { getLinkById, toggleLinkModal, setLinkData },
|
||||
} = useHome();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const linkDetail = getLinkById(linkId);
|
||||
if (!linkDetail) return <></>;
|
||||
@@ -71,6 +73,7 @@ export const ProjectLinkDetail: FC<TProjectLinkDetail> = observer((props) => {
|
||||
icon: Trash2,
|
||||
},
|
||||
];
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(linkDetail.created_at);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -82,7 +85,7 @@ export const ProjectLinkDetail: FC<TProjectLinkDetail> = observer((props) => {
|
||||
</div>
|
||||
<div className="my-auto flex-1">
|
||||
<div className="text-sm font-medium truncate">{linkDetail.title || linkDetail.url}</div>
|
||||
<div className="text-xs font-medium text-custom-text-400">{calculateTimeAgo(linkDetail.created_at)}</div>
|
||||
<div className="text-xs font-medium text-custom-text-400">{t(i18n_time_ago, { time })}</div>
|
||||
</div>
|
||||
|
||||
<CustomMenu
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TActivityEntityData, TIssueEntityData } from "@plane/types";
|
||||
import { LayersIcon, PriorityIcon, StateGroupIcon, Tooltip } from "@plane/ui";
|
||||
import { ListItem } from "@/components/core/list";
|
||||
import { MemberDropdown } from "@/components/dropdowns";
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { useIssueDetail, useProjectState } from "@/hooks/store";
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
|
||||
@@ -16,9 +17,11 @@ export const RecentIssue = (props: BlockProps) => {
|
||||
// hooks
|
||||
const { getStateById } = useProjectState();
|
||||
const { setPeekIssue } = useIssueDetail();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const issueDetails: TIssueEntityData = activity.entity_data as TIssueEntityData;
|
||||
const state = getStateById(issueDetails?.state);
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(activity.visited_at);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -47,7 +50,7 @@ export const RecentIssue = (props: BlockProps) => {
|
||||
</div>
|
||||
)}
|
||||
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">{issueDetails?.name}</div>
|
||||
<div className="font-medium text-xs text-custom-text-400">{calculateTimeAgo(activity.visited_at)}</div>
|
||||
<div className="font-medium text-xs text-custom-text-400">{t(i18n_time_ago, { time })} </div>
|
||||
</div>
|
||||
}
|
||||
quickActionElement={
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FileText } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TActivityEntityData, TPageEntityData } from "@plane/types";
|
||||
import { Avatar, Logo } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
import { ListItem } from "@/components/core/list";
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { useMember } from "@/hooks/store";
|
||||
|
||||
type BlockProps = {
|
||||
@@ -18,9 +19,11 @@ export const RecentPage = (props: BlockProps) => {
|
||||
const router = useRouter();
|
||||
// hooks
|
||||
const { getUserDetails } = useMember();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const pageDetails: TPageEntityData = activity.entity_data as TPageEntityData;
|
||||
const ownerDetails = getUserDetails(pageDetails?.owned_by);
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(activity.visited_at);
|
||||
return (
|
||||
<ListItem
|
||||
key={activity.id}
|
||||
@@ -43,7 +46,7 @@ export const RecentPage = (props: BlockProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">{pageDetails?.name}</div>
|
||||
<div className="font-medium text-xs text-custom-text-400">{calculateTimeAgo(activity.visited_at)}</div>
|
||||
<div className="font-medium text-xs text-custom-text-400">{t(i18n_time_ago, { time })}</div>
|
||||
</div>
|
||||
}
|
||||
quickActionElement={
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TActivityEntityData, TProjectEntityData } from "@plane/types";
|
||||
import { Logo } from "@plane/ui";
|
||||
import { ListItem } from "@/components/core/list";
|
||||
import { MemberDropdown } from "@/components/dropdowns";
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
|
||||
type BlockProps = {
|
||||
activity: TActivityEntityData;
|
||||
@@ -14,8 +15,11 @@ export const RecentProject = (props: BlockProps) => {
|
||||
const { activity, ref, workspaceSlug } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const projectDetails: TProjectEntityData = activity.entity_data as TProjectEntityData;
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(activity.visited_at);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -33,7 +37,7 @@ export const RecentProject = (props: BlockProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">{projectDetails?.name}</div>
|
||||
<div className="font-medium text-xs text-custom-text-400">{calculateTimeAgo(activity.visited_at)}</div>
|
||||
<div className="font-medium text-xs text-custom-text-400">{t(i18n_time_ago, { time })} </div>
|
||||
</div>
|
||||
}
|
||||
quickActionElement={
|
||||
|
||||
+5
-2
@@ -3,8 +3,9 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
import { Network } from "lucide-react";
|
||||
// hooks
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { renderFormattedTime, renderFormattedDate, calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// ui
|
||||
@@ -26,10 +27,12 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
const { isMobile } = usePlatformOS();
|
||||
if (!activity) return <></>;
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(activity.created_at);
|
||||
return (
|
||||
<div
|
||||
className={`relative flex items-center gap-3 text-xs ${
|
||||
@@ -48,7 +51,7 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`}
|
||||
>
|
||||
<span className="whitespace-nowrap"> {calculateTimeAgo(activity.created_at)}</span>
|
||||
<span className="whitespace-nowrap"> {t(i18n_time_ago, { time })}</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
@@ -20,19 +21,23 @@ export const IssueCommentBlock: FC<TIssueCommentBlock> = observer((props) => {
|
||||
const {
|
||||
comment: { getCommentById },
|
||||
} = useIssueDetail();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const comment = getCommentById(commentId);
|
||||
|
||||
if (!comment) return <></>;
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(comment.created_at);
|
||||
return (
|
||||
<div className={`relative flex gap-3 ${ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2`}`}>
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden />
|
||||
<div className="flex-shrink-0 relative w-7 h-7 rounded-full flex justify-center items-center z-[3] bg-gray-500 text-white border border-white uppercase font-medium">
|
||||
{comment.actor_detail.avatar_url && comment.actor_detail.avatar_url !== "" ? (
|
||||
{comment.actor_detail?.avatar_url && comment.actor_detail?.avatar_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(comment.actor_detail.avatar_url)}
|
||||
src={getFileURL(comment.actor_detail?.avatar_url)}
|
||||
alt={
|
||||
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
|
||||
comment.actor_detail?.is_bot
|
||||
? comment.actor_detail?.first_name + " Bot"
|
||||
: comment.actor_detail?.display_name
|
||||
}
|
||||
height={30}
|
||||
width={30}
|
||||
@@ -40,9 +45,9 @@ export const IssueCommentBlock: FC<TIssueCommentBlock> = observer((props) => {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name.charAt(0)
|
||||
: comment.actor_detail.display_name.charAt(0)}
|
||||
{comment.actor_detail?.is_bot
|
||||
? comment.actor_detail?.first_name.charAt(0)
|
||||
: comment.actor_detail?.display_name.charAt(0)}
|
||||
</>
|
||||
)}
|
||||
<div className="absolute top-2 left-4 w-5 h-5 rounded-full overflow-hidden flex justify-center items-center bg-custom-background-80">
|
||||
@@ -53,11 +58,11 @@ export const IssueCommentBlock: FC<TIssueCommentBlock> = observer((props) => {
|
||||
<div className="w-full truncate space-y-1">
|
||||
<div>
|
||||
<div className="text-xs capitalize">
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name + " Bot"
|
||||
: comment.actor_detail.display_name}
|
||||
{comment.actor_detail?.is_bot
|
||||
? comment.actor_detail?.first_name + " Bot"
|
||||
: comment.actor_detail?.display_name}
|
||||
</div>
|
||||
<div className="text-xs text-custom-text-200">commented {calculateTimeAgo(comment.created_at)}</div>
|
||||
<div className="text-xs text-custom-text-200">commented {t(i18n_time_ago, { time })}</div>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,12 @@ import { FC } from "react";
|
||||
// hooks
|
||||
// ui
|
||||
import { Pencil, Trash2, LinkIcon, ExternalLink } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// icons
|
||||
// types
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
import { useIssueDetail, useMember } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
@@ -31,6 +32,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
||||
} = useIssueDetail();
|
||||
const { getUserDetails } = useMember();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
const linkDetail = getLinkById(linkId);
|
||||
if (!linkDetail) return <></>;
|
||||
|
||||
@@ -40,7 +42,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
||||
};
|
||||
|
||||
const createdByDetails = getUserDetails(linkDetail.created_by_id);
|
||||
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(linkDetail.created_at);
|
||||
return (
|
||||
<div key={linkId}>
|
||||
<div className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
|
||||
@@ -107,7 +109,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
||||
|
||||
<div className="px-5">
|
||||
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
|
||||
Added {calculateTimeAgo(linkDetail.created_at)}
|
||||
Added {t(i18n_time_ago, { time })}
|
||||
<br />
|
||||
{createdByDetails && (
|
||||
<>
|
||||
|
||||
@@ -8,12 +8,13 @@ import { TIssueServiceType } from "@plane/types";
|
||||
// ui
|
||||
import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui";
|
||||
// helpers
|
||||
import { calculateTimeAgoShort } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgoShort } from "@/helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { TLinkOperationsModal } from "./create-update-link-modal";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
||||
type TIssueLinkItem = {
|
||||
linkId: string;
|
||||
@@ -32,6 +33,7 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
|
||||
link: { getLinkById },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
const linkDetail = getLinkById(linkId);
|
||||
if (!linkDetail) return <></>;
|
||||
|
||||
@@ -39,6 +41,7 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
|
||||
toggleIssueLinkModalStore(modalToggle);
|
||||
setIssueLinkData(linkDetail);
|
||||
};
|
||||
const { i18n_key, time } = calculateI18nTimeAgoShort(linkDetail.created_at);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -60,7 +63,8 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<p className="p-1 text-xs align-bottom leading-5 text-custom-text-400 group-hover-text-custom-text-200">
|
||||
{calculateTimeAgoShort(linkDetail.created_at)}
|
||||
{time}
|
||||
{t(i18n_key)}
|
||||
</p>
|
||||
<span
|
||||
onClick={() => {
|
||||
|
||||
@@ -53,20 +53,26 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
} = useIssueDetail(issueServiceType);
|
||||
const project = useProject();
|
||||
const { getProjectStates } = useProjectState();
|
||||
const { handleRedirection } = useIssuePeekOverviewRedirection();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
// derived values
|
||||
const issue = getIssueById(relationIssueId);
|
||||
const { handleRedirection } = useIssuePeekOverviewRedirection(!!issue?.is_epic);
|
||||
const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined;
|
||||
const currentIssueStateDetail =
|
||||
(issue?.project_id && getProjectStates(issue?.project_id)?.find((state) => issue?.state_id == state.id)) ||
|
||||
undefined;
|
||||
|
||||
if (!issue) return <></>;
|
||||
const issueLink = `/${workspaceSlug}/projects/${projectId}/${issue.is_epic ? "epics" : "issues"}/${issue.id}`;
|
||||
|
||||
// handlers
|
||||
const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug, issue, isMobile);
|
||||
const handleIssuePeekOverview = (issue: TIssue) => {
|
||||
if (issueServiceType === EIssueServiceType.ISSUES && issue.is_epic) {
|
||||
// open epics in new tab
|
||||
window.open(issueLink, "_blank");
|
||||
return;
|
||||
}
|
||||
handleRedirection(workspaceSlug, issue, isMobile);
|
||||
};
|
||||
|
||||
const handleEditIssue = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
@@ -85,7 +91,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
const handleCopyIssueLink = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
issueOperations.copyText(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`);
|
||||
issueOperations.copyText(issueLink);
|
||||
};
|
||||
|
||||
const handleRemoveRelation = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
@@ -98,7 +104,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
<div key={relationIssueId}>
|
||||
<ControlLink
|
||||
id={`issue-${issue.id}`}
|
||||
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
|
||||
href={issueLink}
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { Copy, LinkIcon, Pencil, Trash2 } from "lucide-react";
|
||||
// plane types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ILinkDetails } from "@plane/types";
|
||||
// plane ui
|
||||
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
@@ -22,6 +23,7 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
|
||||
const { handleDeleteLink, handleEditLink, isEditingAllowed, link } = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const createdByDetails = getUserDetails(link.created_by);
|
||||
// platform os
|
||||
@@ -36,6 +38,7 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
|
||||
})
|
||||
);
|
||||
};
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(link.created_at);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
|
||||
@@ -88,7 +91,7 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
|
||||
Added {calculateTimeAgo(link.created_at)}
|
||||
Added {t(i18n_time_ago, { time })}
|
||||
<br />
|
||||
{createdByDetails && (
|
||||
<>by {createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}</>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { observer } from "mobx-react";
|
||||
// helpers
|
||||
import { calculateTimeAgoShort } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgoShort } from "@/helpers/date-time.helper";
|
||||
// store types
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
||||
type Props = {
|
||||
page: TPageInstance;
|
||||
@@ -10,10 +11,15 @@ type Props = {
|
||||
|
||||
export const PageEditInformationPopover: React.FC<Props> = observer((props) => {
|
||||
const { page } = props;
|
||||
const { t } = useTranslation();
|
||||
const { i18n_key, time } = page.updated_at ? calculateI18nTimeAgoShort(page.updated_at) : { i18n_key: "", time: 0 };
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 whitespace-nowrap">
|
||||
<span className="text-sm text-custom-text-300">Edited {calculateTimeAgoShort(page.updated_at ?? "")} ago</span>
|
||||
<span className="text-sm text-custom-text-300">
|
||||
Edited {time}
|
||||
{t(i18n_key)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { History, MessageSquare } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IUserActivityResponse } from "@plane/types";
|
||||
// hooks
|
||||
// components
|
||||
@@ -12,7 +13,7 @@ import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/ric
|
||||
// ui
|
||||
import { ActivitySettingsLoader } from "@/components/ui";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
@@ -27,6 +28,7 @@ export const ActivityList: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// TODO: refactor this component
|
||||
return (
|
||||
@@ -34,6 +36,7 @@ export const ActivityList: React.FC<Props> = observer((props) => {
|
||||
{activity ? (
|
||||
<ul role="list">
|
||||
{activity.results.map((activityItem) => {
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(activityItem.created_at);
|
||||
if (activityItem.field === "comment")
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-2">
|
||||
@@ -66,9 +69,7 @@ export const ActivityList: React.FC<Props> = observer((props) => {
|
||||
? activityItem.actor_detail.first_name + " Bot"
|
||||
: activityItem.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {calculateTimeAgo(activityItem.created_at)}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">Commented {t(i18n_time_ago, { time })}</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<RichTextReadOnlyEditor
|
||||
@@ -155,9 +156,7 @@ export const ActivityList: React.FC<Props> = observer((props) => {
|
||||
)}{" "}
|
||||
<div className="inline gap-1">
|
||||
{message}{" "}
|
||||
<span className="flex-shrink-0 whitespace-nowrap">
|
||||
{calculateTimeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
<span className="flex-shrink-0 whitespace-nowrap">{t(i18n_time_ago, { time })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,13 +5,14 @@ import useSWR from "swr";
|
||||
// icons
|
||||
import { History, MessageSquare } from "lucide-react";
|
||||
// hooks
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||
import { ActivitySettingsLoader } from "@/components/ui";
|
||||
// constants
|
||||
import { USER_ACTIVITY } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
@@ -32,7 +33,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
const { cursor, perPage, updateResultsCount, updateTotalPages, updateEmptyState } = props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { data: userProfileActivity } = useSWR(
|
||||
USER_ACTIVITY({
|
||||
cursor,
|
||||
@@ -60,6 +61,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
{userProfileActivity ? (
|
||||
<ul role="list">
|
||||
{userProfileActivity.results.map((activityItem: any) => {
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(activityItem.created_at);
|
||||
if (activityItem.field === "comment")
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-2">
|
||||
@@ -92,9 +94,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
? activityItem.actor_detail.first_name + " Bot"
|
||||
: activityItem.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {calculateTimeAgo(activityItem.created_at)}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">Commented {t(i18n_time_ago, { time })}</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<RichTextReadOnlyEditor
|
||||
@@ -177,9 +177,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
)}{" "}
|
||||
<div className="inline gap-1">
|
||||
{message}{" "}
|
||||
<span className="flex-shrink-0 whitespace-nowrap">
|
||||
{calculateTimeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
<span className="flex-shrink-0 whitespace-nowrap">{t(i18n_time_ago, { time })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Loader, Card } from "@plane/ui";
|
||||
// components
|
||||
import { ActivityMessage, IssueLink } from "@/components/core";
|
||||
@@ -11,7 +12,7 @@ import { ProfileEmptyState } from "@/components/ui";
|
||||
// constants
|
||||
import { USER_PROFILE_ACTIVITY } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
@@ -25,6 +26,7 @@ const userService = new UserService();
|
||||
export const ProfileActivity = observer(() => {
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
const { data: userProfileActivity } = useSWR(
|
||||
@@ -44,40 +46,45 @@ export const ProfileActivity = observer(() => {
|
||||
{userProfileActivity ? (
|
||||
userProfileActivity.results.length > 0 ? (
|
||||
<div className="space-y-5">
|
||||
{userProfileActivity.results.map((activity) => (
|
||||
<div key={activity.id} className="flex gap-3">
|
||||
<div className="flex-shrink-0 grid place-items-center overflow-hidden rounded h-6 w-6">
|
||||
{activity.actor_detail?.avatar_url && activity.actor_detail?.avatar_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(activity.actor_detail?.avatar_url)}
|
||||
alt={activity.actor_detail?.display_name}
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
|
||||
{activity.actor_detail?.display_name?.charAt(0)}
|
||||
{userProfileActivity.results.map((activity) => {
|
||||
{
|
||||
const { i18n_time_ago, time } = calculateI18nTimeAgo(activity.created_at);
|
||||
return (
|
||||
<div key={activity.id} className="flex gap-3">
|
||||
<div className="flex-shrink-0 grid place-items-center overflow-hidden rounded h-6 w-6">
|
||||
{activity.actor_detail?.avatar_url && activity.actor_detail?.avatar_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(activity.actor_detail?.avatar_url)}
|
||||
alt={activity.actor_detail?.display_name}
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
|
||||
{activity.actor_detail?.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mt-1 w-4/5 break-words">
|
||||
<p className="inline text-sm text-custom-text-200">
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{currentUser?.id === activity.actor_detail?.id ? "You" : activity.actor_detail?.display_name}{" "}
|
||||
</span>
|
||||
{activity.field ? (
|
||||
<ActivityMessage activity={activity} showIssue />
|
||||
) : (
|
||||
<span>
|
||||
created <IssueLink activity={activity} />
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-custom-text-200 whitespace-nowrap ">
|
||||
{calculateTimeAgo(activity.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="-mt-1 w-4/5 break-words">
|
||||
<p className="inline text-sm text-custom-text-200">
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{currentUser?.id === activity.actor_detail?.id
|
||||
? "You"
|
||||
: activity.actor_detail?.display_name}{" "}
|
||||
</span>
|
||||
{activity.field ? (
|
||||
<ActivityMessage activity={activity} showIssue />
|
||||
) : (
|
||||
<span>
|
||||
created <IssueLink activity={activity} />
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-custom-text-200 whitespace-nowrap ">{t(i18n_time_ago, { time })}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<ProfileEmptyState
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
||||
interface IStickyDelete {
|
||||
isOpen: boolean;
|
||||
handleSubmit: () => void;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ export const StickyDeleteModal: React.FC<IStickyDelete> = observer((props) => {
|
||||
try {
|
||||
setLoader(true);
|
||||
await handleSubmit();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Warning!",
|
||||
message: "Something went wrong please try again later.",
|
||||
message: "Something went wrong. Please try again later.",
|
||||
});
|
||||
} finally {
|
||||
setLoader(false);
|
||||
@@ -38,7 +38,7 @@ export const StickyDeleteModal: React.FC<IStickyDelete> = observer((props) => {
|
||||
isSubmitting={loader}
|
||||
isOpen={isOpen}
|
||||
title="Delete sticky"
|
||||
content={<>Are you sure you want to delete the sticky? </>}
|
||||
content="Are you sure you want to delete the sticky?"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,18 +7,17 @@ import type { ElementDragPayload } from "@atlaskit/pragmatic-drag-and-drop/eleme
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Masonry from "react-masonry-component";
|
||||
// plane ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { StickiesEmptyState } from "@/components/home/widgets/empty-states/stickies";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// hooks
|
||||
import { useSticky } from "@/hooks/use-stickies";
|
||||
import { useStickyOperations } from "../sticky/use-operations";
|
||||
import { StickiesLoader } from "./stickies-loader";
|
||||
import { StickyDNDWrapper } from "./sticky-dnd-wrapper";
|
||||
import { getInstructionFromPayload } from "./sticky.helpers";
|
||||
import { StickiesEmptyState } from "@/components/home/widgets/empty-states/stickies";
|
||||
|
||||
type TStickiesLayout = {
|
||||
workspaceSlug: string;
|
||||
@@ -42,6 +41,14 @@ export const StickiesList = observer((props: TProps) => {
|
||||
const itemWidth = `${100 / columnCount}%`;
|
||||
const totalRows = Math.ceil(workspaceStickyIds.length / columnCount);
|
||||
const isStickiesPage = pathname?.includes("stickies");
|
||||
const masonryRef = useRef<any>(null);
|
||||
|
||||
const handleLayout = () => {
|
||||
if (masonryRef.current) {
|
||||
// Force reflow
|
||||
masonryRef.current.performLayout();
|
||||
}
|
||||
};
|
||||
|
||||
// Function to determine if an item is in first or last row
|
||||
const getRowPositions = (index: number) => {
|
||||
@@ -72,19 +79,13 @@ export const StickiesList = observer((props: TProps) => {
|
||||
};
|
||||
|
||||
if (loader === "init-loader") {
|
||||
return (
|
||||
<div className="min-h-[500px] overflow-scroll pb-2">
|
||||
<Loader>
|
||||
<Loader.Item height="300px" width="255px" />
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
return <StickiesLoader />;
|
||||
}
|
||||
|
||||
if (loader === "loaded" && workspaceStickyIds.length === 0) {
|
||||
return (
|
||||
<div className="size-full grid place-items-center">
|
||||
{isStickiesPage ? (
|
||||
{isStickiesPage || searchQuery ? (
|
||||
<EmptyState
|
||||
type={searchQuery ? EmptyStateType.STICKIES_SEARCH : EmptyStateType.STICKIES}
|
||||
layout={searchQuery ? "screen-simple" : "screen-detailed"}
|
||||
@@ -106,7 +107,7 @@ export const StickiesList = observer((props: TProps) => {
|
||||
return (
|
||||
<div className="transition-opacity duration-300 ease-in-out">
|
||||
{/* @ts-expect-error type mismatch here */}
|
||||
<Masonry elementType="div">
|
||||
<Masonry elementType="div" ref={masonryRef}>
|
||||
{workspaceStickyIds.map((stickyId, index) => {
|
||||
const { isInFirstRow, isInLastRow } = getRowPositions(index);
|
||||
return (
|
||||
@@ -119,6 +120,7 @@ export const StickiesList = observer((props: TProps) => {
|
||||
isLastChild={index === workspaceStickyIds.length - 1}
|
||||
isInFirstRow={isInFirstRow}
|
||||
isInLastRow={isInLastRow}
|
||||
handleLayout={handleLayout}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// plane ui
|
||||
import { Loader } from "@plane/ui";
|
||||
|
||||
export const StickiesLoader = () => (
|
||||
<div className="overflow-scroll pb-2 grid grid-cols-4 gap-4">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<Loader key={index} className="space-y-5 border border-custom-border-200 p-3 rounded">
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="15px" width="75%" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item height="15px" width="15px" className="flex-shrink-0" />
|
||||
<Loader.Item height="15px" width="100%" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item height="15px" width="15px" className="flex-shrink-0" />
|
||||
<Loader.Item height="15px" width="75%" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item height="15px" width="15px" className="flex-shrink-0" />
|
||||
<Loader.Item height="15px" width="90%" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item height="15px" width="15px" className="flex-shrink-0" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item height="15px" width="15px" className="flex-shrink-0" />
|
||||
<Loader.Item height="15px" width="50%" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item height="25px" width="25px" />
|
||||
<Loader.Item height="25px" width="25px" />
|
||||
<Loader.Item height="25px" width="25px" />
|
||||
</div>
|
||||
<Loader.Item height="25px" width="25px" className="flex-shrink-0" />
|
||||
</div>
|
||||
</Loader>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -10,7 +10,12 @@ import { useSticky } from "@/hooks/use-stickies";
|
||||
import { ContentOverflowWrapper } from "../../core/content-overflow-HOC";
|
||||
import { StickiesLayout } from "./stickies-list";
|
||||
|
||||
export const StickiesTruncated = observer(() => {
|
||||
type StickiesTruncatedProps = {
|
||||
handleClose?: () => void;
|
||||
};
|
||||
|
||||
export const StickiesTruncated = observer((props: StickiesTruncatedProps) => {
|
||||
const { handleClose = () => {} } = props;
|
||||
// navigation
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
@@ -33,6 +38,7 @@ export const StickiesTruncated = observer(() => {
|
||||
className={cn(
|
||||
"gap-1 w-full text-custom-primary-100 text-sm font-medium transition-opacity duration-300 bg-custom-background-90/20"
|
||||
)}
|
||||
onClick={handleClose}
|
||||
>
|
||||
Show all
|
||||
</Link>
|
||||
|
||||
@@ -15,123 +15,121 @@ import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { createRoot } from "react-dom/client";
|
||||
// plane types
|
||||
import { InstructionType } from "@plane/types";
|
||||
// plane ui
|
||||
import { DropIndicator } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { StickyNote } from "../sticky";
|
||||
// helpers
|
||||
import { getInstructionFromPayload } from "./sticky.helpers";
|
||||
|
||||
// Draggable Sticky Wrapper Component
|
||||
export const StickyDNDWrapper = observer(
|
||||
({
|
||||
stickyId,
|
||||
workspaceSlug,
|
||||
itemWidth,
|
||||
isLastChild,
|
||||
isInFirstRow,
|
||||
isInLastRow,
|
||||
handleDrop,
|
||||
}: {
|
||||
stickyId: string;
|
||||
workspaceSlug: string;
|
||||
itemWidth: string;
|
||||
isLastChild: boolean;
|
||||
isInFirstRow: boolean;
|
||||
isInLastRow: boolean;
|
||||
handleDrop: (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => void;
|
||||
}) => {
|
||||
const pathName = usePathname();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [instruction, setInstruction] = useState<InstructionType | undefined>(undefined);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
type Props = {
|
||||
stickyId: string;
|
||||
workspaceSlug: string;
|
||||
itemWidth: string;
|
||||
isLastChild: boolean;
|
||||
isInFirstRow: boolean;
|
||||
isInLastRow: boolean;
|
||||
handleDrop: (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => void;
|
||||
handleLayout: () => void;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
export const StickyDNDWrapper = observer((props: Props) => {
|
||||
const { stickyId, workspaceSlug, itemWidth, isLastChild, isInFirstRow, isInLastRow, handleDrop, handleLayout } =
|
||||
props;
|
||||
// states
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [instruction, setInstruction] = useState<InstructionType | undefined>(undefined);
|
||||
// refs
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
// navigation
|
||||
const pathname = usePathname();
|
||||
|
||||
const initialData = { id: stickyId, type: "sticky" };
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
|
||||
if (pathName.includes("stickies"))
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
dragHandle: element,
|
||||
getInitialData: () => initialData,
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
onGenerateDragPreview: ({ nativeSetDragImage }) => {
|
||||
setCustomNativeDragPreview({
|
||||
getOffset: pointerOutsideOfPreview({ x: "-200px", y: "0px" }),
|
||||
render: ({ container }) => {
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<div className="scale-50">
|
||||
<div className="-m-2 max-h-[150px]">
|
||||
<StickyNote
|
||||
className={"w-[290px]"}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
stickyId={stickyId}
|
||||
showToolbar={false}
|
||||
/>
|
||||
</div>
|
||||
const initialData = { id: stickyId, type: "sticky" };
|
||||
|
||||
if (pathname.includes("stickies"))
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
dragHandle: element,
|
||||
getInitialData: () => initialData,
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
onGenerateDragPreview: ({ nativeSetDragImage }) => {
|
||||
setCustomNativeDragPreview({
|
||||
getOffset: pointerOutsideOfPreview({ x: "-200px", y: "0px" }),
|
||||
render: ({ container }) => {
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<div className="scale-50">
|
||||
<div className="-m-2 max-h-[150px]">
|
||||
<StickyNote
|
||||
className={"w-[290px]"}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
stickyId={stickyId}
|
||||
showToolbar={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return () => root.unmount();
|
||||
},
|
||||
nativeSetDragImage,
|
||||
});
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop: ({ source }) => source.data?.type === "sticky",
|
||||
getData: ({ input, element }) => {
|
||||
const blockedStates: InstructionType[] = ["make-child"];
|
||||
if (!isLastChild) {
|
||||
blockedStates.push("reorder-below");
|
||||
}
|
||||
</div>
|
||||
);
|
||||
return () => root.unmount();
|
||||
},
|
||||
nativeSetDragImage,
|
||||
});
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop: ({ source }) => source.data?.type === "sticky",
|
||||
getData: ({ input, element }) => {
|
||||
const blockedStates: InstructionType[] = ["make-child"];
|
||||
if (!isLastChild) {
|
||||
blockedStates.push("reorder-below");
|
||||
}
|
||||
|
||||
return attachInstruction(initialData, {
|
||||
input,
|
||||
element,
|
||||
currentLevel: 1,
|
||||
indentPerLevel: 0,
|
||||
mode: isLastChild ? "last-in-group" : "standard",
|
||||
block: blockedStates,
|
||||
});
|
||||
},
|
||||
onDrag: ({ self, source, location }) => {
|
||||
const instruction = getInstructionFromPayload(self, source, location);
|
||||
setInstruction(instruction);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setInstruction(undefined);
|
||||
},
|
||||
onDrop: ({ self, source, location }) => {
|
||||
setInstruction(undefined);
|
||||
handleDrop(self, source, location);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [stickyId, isDragging]);
|
||||
return attachInstruction(initialData, {
|
||||
input,
|
||||
element,
|
||||
currentLevel: 1,
|
||||
indentPerLevel: 0,
|
||||
mode: isLastChild ? "last-in-group" : "standard",
|
||||
block: blockedStates,
|
||||
});
|
||||
},
|
||||
onDrag: ({ self, source, location }) => {
|
||||
const instruction = getInstructionFromPayload(self, source, location);
|
||||
setInstruction(instruction);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setInstruction(undefined);
|
||||
},
|
||||
onDrop: ({ self, source, location }) => {
|
||||
setInstruction(undefined);
|
||||
handleDrop(self, source, location);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [handleDrop, isDragging, isLastChild, pathname, stickyId, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: itemWidth }}>
|
||||
{!isInFirstRow && <DropIndicator isVisible={instruction === "reorder-above"} />}
|
||||
<div
|
||||
ref={elementRef}
|
||||
className={cn("flex min-h-[300px] box-border p-2", {
|
||||
"opacity-50": isDragging,
|
||||
})}
|
||||
>
|
||||
<StickyNote key={stickyId || "new"} workspaceSlug={workspaceSlug} stickyId={stickyId} />
|
||||
</div>
|
||||
{!isInLastRow && <DropIndicator isVisible={instruction === "reorder-below"} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div className="flex min-h-[300px] box-border p-2 flex-col" style={{ width: itemWidth }}>
|
||||
{!isInFirstRow && <DropIndicator isVisible={instruction === "reorder-above"} />}
|
||||
<StickyNote
|
||||
key={stickyId || "new"}
|
||||
workspaceSlug={workspaceSlug}
|
||||
stickyId={stickyId}
|
||||
handleLayout={handleLayout}
|
||||
/>
|
||||
{!isInLastRow && <DropIndicator isVisible={instruction === "reorder-below"} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Plus, X } from "lucide-react";
|
||||
// plane ui
|
||||
import { RecentStickyIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useSticky } from "@/hooks/use-stickies";
|
||||
// components
|
||||
import { StickiesTruncated } from "../layout/stickies-truncated";
|
||||
import { useStickyOperations } from "../sticky/use-operations";
|
||||
import { StickySearch } from "./search";
|
||||
@@ -13,8 +16,11 @@ type TProps = {
|
||||
|
||||
export const Stickies = observer((props: TProps) => {
|
||||
const { handleClose } = props;
|
||||
// navigation
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { creatingSticky, toggleShowNewSticky } = useSticky();
|
||||
// sticky operations
|
||||
const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() });
|
||||
|
||||
return (
|
||||
@@ -22,9 +28,9 @@ export const Stickies = observer((props: TProps) => {
|
||||
{/* header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
{/* Title */}
|
||||
<div className="text-custom-text-100 flex gap-2">
|
||||
<RecentStickyIcon className="size-5 rotate-90" />
|
||||
<p className="text-lg font-medium">Your stickies</p>
|
||||
<div className="text-custom-text-200 flex items-center gap-2">
|
||||
<RecentStickyIcon className="size-5 rotate-90 flex-shrink-0" />
|
||||
<p className="text-xl font-medium">Your stickies</p>
|
||||
</div>
|
||||
{/* actions */}
|
||||
<div className="flex gap-2">
|
||||
@@ -50,6 +56,7 @@ export const Stickies = observer((props: TProps) => {
|
||||
</button>
|
||||
{handleClose && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="flex-shrink-0 grid place-items-center text-custom-text-300 hover:text-custom-text-100 hover:bg-custom-background-80 rounded p-1 transition-colors my-auto"
|
||||
>
|
||||
@@ -60,7 +67,7 @@ export const Stickies = observer((props: TProps) => {
|
||||
</div>
|
||||
{/* content */}
|
||||
<div className="mb-4 max-h-[625px] overflow-scroll">
|
||||
<StickiesTruncated />
|
||||
<StickiesTruncated handleClose={handleClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { DebouncedFunc } from "lodash";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// plane editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// plane types
|
||||
import { TSticky } from "@plane/types";
|
||||
// plane utils
|
||||
import { isCommentEmpty } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
// components
|
||||
import { StickyEditor } from "../../editor";
|
||||
|
||||
type TProps = {
|
||||
stickyData: TSticky | undefined;
|
||||
stickyData: Partial<TSticky> | undefined;
|
||||
workspaceSlug: string;
|
||||
handleUpdate: DebouncedFunc<(payload: Partial<TSticky>) => Promise<void>>;
|
||||
handleUpdate: (payload: Partial<TSticky>) => void;
|
||||
stickyId: string | undefined;
|
||||
showToolbar?: boolean;
|
||||
handleChange: (data: Partial<TSticky>) => Promise<void>;
|
||||
@@ -68,7 +69,11 @@ export const StickyInput = (props: TProps) => {
|
||||
onChange(description_html);
|
||||
handleSubmit(handleFormSubmit)();
|
||||
}}
|
||||
placeholder="Click to type here"
|
||||
placeholder={(_, value) => {
|
||||
const isContentEmpty = isCommentEmpty(value);
|
||||
if (!isContentEmpty) return "";
|
||||
return "Click to type here";
|
||||
}}
|
||||
containerClassName="px-0 text-base min-h-[250px] w-full"
|
||||
uploadFile={async () => ""}
|
||||
showToolbar={showToolbar}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Minimize2 } from "lucide-react";
|
||||
// plane types
|
||||
import { TSticky } from "@plane/types";
|
||||
@@ -13,8 +12,7 @@ import { useSticky } from "@/hooks/use-stickies";
|
||||
import { STICKY_COLORS_LIST } from "../../editor/sticky-editor/color-palette";
|
||||
import { StickyDeleteModal } from "../delete-modal";
|
||||
import { StickyInput } from "./inputs";
|
||||
import { StickyItemDragHandle } from "./sticky-item-drag-handle";
|
||||
import { useStickyOperations } from "./use-operations";
|
||||
import { getRandomStickyColor, useStickyOperations } from "./use-operations";
|
||||
|
||||
type TProps = {
|
||||
onClose?: () => void;
|
||||
@@ -22,11 +20,12 @@ type TProps = {
|
||||
className?: string;
|
||||
stickyId: string | undefined;
|
||||
showToolbar?: boolean;
|
||||
handleLayout?: () => void;
|
||||
};
|
||||
export const StickyNote = observer((props: TProps) => {
|
||||
const { onClose, workspaceSlug, className = "", stickyId, showToolbar } = props;
|
||||
const { onClose, workspaceSlug, className = "", stickyId, showToolbar, handleLayout } = props;
|
||||
// navigation
|
||||
const pathName = usePathname();
|
||||
// const pathName = usePathname();
|
||||
// states
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
// store hooks
|
||||
@@ -34,8 +33,8 @@ export const StickyNote = observer((props: TProps) => {
|
||||
// sticky operations
|
||||
const { stickyOperations } = useStickyOperations({ workspaceSlug });
|
||||
// derived values
|
||||
const stickyData = stickyId ? stickies[stickyId] : undefined;
|
||||
const isStickiesPage = pathName?.includes("stickies");
|
||||
const stickyData: Partial<TSticky> = stickyId ? stickies[stickyId] : { background_color: getRandomStickyColor() };
|
||||
// const isStickiesPage = pathName?.includes("stickies");
|
||||
const backgroundColor =
|
||||
STICKY_COLORS_LIST.find((c) => c.key === stickyData?.background_color)?.backgroundColor ||
|
||||
STICKY_COLORS_LIST[0].backgroundColor;
|
||||
@@ -46,6 +45,7 @@ export const StickyNote = observer((props: TProps) => {
|
||||
await stickyOperations.update(stickyId, payload);
|
||||
} else {
|
||||
await stickyOperations.create({
|
||||
...stickyData,
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
@@ -74,12 +74,12 @@ export const StickyNote = observer((props: TProps) => {
|
||||
handleClose={() => setIsDeleteModalOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className={cn("w-full flex flex-col h-fit rounded p-4 group/sticky", className)}
|
||||
className={cn("w-full flex flex-col h-fit rounded p-4 group/sticky max-h-[650px] overflow-y-scroll", className)}
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
{isStickiesPage && <StickyItemDragHandle isDragging={false} />}{" "}
|
||||
{/* {isStickiesPage && <StickyItemDragHandle isDragging={false} />}{" "} */}
|
||||
{onClose && (
|
||||
<button type="button" className="flex w-full" onClick={onClose}>
|
||||
<Minimize2 className="size-4 m-auto mr-0" />
|
||||
@@ -89,7 +89,10 @@ export const StickyNote = observer((props: TProps) => {
|
||||
<StickyInput
|
||||
stickyData={stickyData}
|
||||
workspaceSlug={workspaceSlug}
|
||||
handleUpdate={debouncedFormSave}
|
||||
handleUpdate={(payload) => {
|
||||
handleLayout?.();
|
||||
debouncedFormSave(payload);
|
||||
}}
|
||||
stickyId={stickyId}
|
||||
handleDelete={() => setIsDeleteModalOpen(true)}
|
||||
handleChange={handleChange}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Clock } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Avatar, Row } from "@plane/ui";
|
||||
// components
|
||||
import { NotificationOption } from "@/components/workspace-notifications";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper";
|
||||
import { calculateI18nTimeAgo, renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
|
||||
import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
|
||||
@@ -26,6 +27,7 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
||||
const { currentSelectedNotificationId, setCurrentSelectedNotificationId } = useWorkspaceNotifications();
|
||||
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
|
||||
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [isSnoozeStateModalOpen, setIsSnoozeStateModalOpen] = useState(false);
|
||||
const [customSnoozeModal, setCustomSnoozeModal] = useState(false);
|
||||
@@ -56,6 +58,9 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
const { i18n_time_ago, time } = notification?.created_at
|
||||
? calculateI18nTimeAgo(notification?.created_at)
|
||||
: { i18n_time_ago: "", time: "" };
|
||||
|
||||
if (!workspaceSlug || !notificationId || !notification?.id || !notificationField) return <></>;
|
||||
|
||||
@@ -165,7 +170,7 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-auto flex-shrink-0 text-custom-text-300">
|
||||
{notification.created_at && calculateTimeAgo(notification.created_at)}
|
||||
{notification.created_at && t(i18n_time_ago, { time })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EUserPermissions } from "ee/constants/user-permissions";
|
||||
import { Plus, Shapes } from "lucide-react";
|
||||
import { EUserPermissions } from "ee/constants/user-permissions";
|
||||
|
||||
export interface EmptyStateDetails {
|
||||
key: EmptyStateType;
|
||||
|
||||
@@ -21,12 +21,13 @@ export class StickyService extends APIService {
|
||||
async getStickies(
|
||||
workspaceSlug: string,
|
||||
cursor: string,
|
||||
query?: string
|
||||
query?: string,
|
||||
per_page?: number
|
||||
): Promise<{ results: TSticky[]; total_pages: number }> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/stickies/`, {
|
||||
params: {
|
||||
cursor,
|
||||
per_page: STICKIES_PER_PAGE,
|
||||
per_page: per_page || STICKIES_PER_PAGE,
|
||||
query,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -96,7 +96,7 @@ export class StickyStore implements IStickyStore {
|
||||
};
|
||||
|
||||
fetchRecentSticky = async (workspaceSlug: string) => {
|
||||
const response = await this.stickyService.getStickies(workspaceSlug, "1:0:0");
|
||||
const response = await this.stickyService.getStickies(workspaceSlug, "1:0:0", undefined, 1);
|
||||
runInAction(() => {
|
||||
this.recentStickyId = response.results[0]?.id;
|
||||
this.stickies[response.results[0]?.id] = response.results[0];
|
||||
|
||||
@@ -156,12 +156,14 @@ export const findHowManyDaysLeft = (
|
||||
};
|
||||
|
||||
// Time Difference Helpers
|
||||
|
||||
/**
|
||||
* @returns {string} formatted date in the form of amount of time passed since the event happened
|
||||
* @description Returns time passed since the event happened
|
||||
* @param {string | Date} time
|
||||
* @example calculateTimeAgo("2023-01-01") // 1 year ago
|
||||
*/
|
||||
|
||||
export const calculateTimeAgo = (time: string | number | Date | null): string => {
|
||||
if (!time) return "";
|
||||
// Parse the time to check if it is valid
|
||||
@@ -173,6 +175,143 @@ export const calculateTimeAgo = (time: string | number | Date | null): string =>
|
||||
return distance;
|
||||
};
|
||||
|
||||
export const calculateI18nTimeAgo = (time: string | number | Date | null): TimeDistanceResult => {
|
||||
if (!time) return { i18n_time_ago: "", time: null };
|
||||
|
||||
const parsedTime = typeof time === "string" || typeof time === "number" ? parseISO(String(time)) : time;
|
||||
|
||||
if (!parsedTime) return { i18n_time_ago: "", time: null };
|
||||
|
||||
const diffInSeconds = (Date.now() - parsedTime.getTime()) / 1000;
|
||||
const distance = getTimeDistance(Math.abs(diffInSeconds));
|
||||
|
||||
return distance;
|
||||
};
|
||||
interface TimeDistanceResult {
|
||||
i18n_time_ago: string;
|
||||
time: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates relative time distance and returns appropriate translation keys
|
||||
*/
|
||||
function getTimeDistance(diffInSeconds: number): TimeDistanceResult {
|
||||
const absSeconds = Math.abs(diffInSeconds);
|
||||
const direction = diffInSeconds > 0 ? "past" : "future";
|
||||
|
||||
// Convert to larger time units
|
||||
const minutes = Math.round(absSeconds / 60);
|
||||
const hours = Math.round(absSeconds / 3600);
|
||||
const days = Math.round(absSeconds / 86400);
|
||||
const months = Math.round(absSeconds / 2592000);
|
||||
const years = Math.round(absSeconds / 31536000);
|
||||
|
||||
// Seconds-based intervals (<1 minute)
|
||||
if (absSeconds < 30)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.less_than_x_seconds.${direction}`,
|
||||
time: 30,
|
||||
};
|
||||
if (absSeconds < 5)
|
||||
return {
|
||||
i18n_time_ago: `date_time.include_seconds.less_than_5_seconds.${direction}`,
|
||||
time: null,
|
||||
};
|
||||
if (absSeconds < 10)
|
||||
return {
|
||||
i18n_time_ago: `date_time.include_seconds.less_than_10_seconds.${direction}`,
|
||||
time: null,
|
||||
};
|
||||
if (absSeconds < 20)
|
||||
return {
|
||||
i18n_time_ago: `date_time.include_seconds.less_than_20_seconds.${direction}`,
|
||||
time: null,
|
||||
};
|
||||
if (absSeconds < 40)
|
||||
return {
|
||||
i18n_time_ago: `date_time.include_seconds.half_a_minute.${direction}`,
|
||||
time: null,
|
||||
};
|
||||
if (absSeconds < 60)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.less_than_x_minutes.${direction}`,
|
||||
time: null,
|
||||
};
|
||||
|
||||
// Minutes to Years
|
||||
if (absSeconds < 90)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.x_minutes.${direction}`,
|
||||
time: 1,
|
||||
};
|
||||
if (absSeconds < 2670)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.x_minutes.${direction}`,
|
||||
time: minutes,
|
||||
};
|
||||
if (absSeconds < 5370)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.about_x_hours.${direction}`,
|
||||
time: 1,
|
||||
};
|
||||
if (absSeconds < 86370)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.about_x_hours.${direction}`,
|
||||
time: hours,
|
||||
};
|
||||
if (absSeconds < 151200)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.x_days.${direction}`,
|
||||
time: 1,
|
||||
};
|
||||
if (absSeconds < 2592000)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.x_days.${direction}`,
|
||||
time: days,
|
||||
};
|
||||
if (absSeconds < 3888000)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.about_x_months.${direction}`,
|
||||
time: 1,
|
||||
};
|
||||
if (absSeconds < 31536000)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.about_x_months.${direction}`,
|
||||
time: months,
|
||||
};
|
||||
if (absSeconds < 47304000)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.about_x_years.${direction}`,
|
||||
time: 1,
|
||||
};
|
||||
if (absSeconds < 56160000)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.over_x_years.${direction}`,
|
||||
time: 1,
|
||||
};
|
||||
if (absSeconds < 63072000)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.almost_x_years.${direction}`,
|
||||
time: 2,
|
||||
};
|
||||
|
||||
// Multiple years
|
||||
if (months % 12 < 3)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.about_x_years.${direction}`,
|
||||
time: years,
|
||||
};
|
||||
if (months % 12 < 9)
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.over_x_years.${direction}`,
|
||||
time: years,
|
||||
};
|
||||
return {
|
||||
i18n_time_ago: `date_time.standard.almost_x_years.${direction}`,
|
||||
time: years + 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateTimeAgoShort(date: string | number | Date | null): string {
|
||||
if (!date) {
|
||||
return "";
|
||||
@@ -210,6 +349,67 @@ export function calculateTimeAgoShort(date: string | number | Date | null): stri
|
||||
return `${Math.floor(diffInYears)}y`;
|
||||
}
|
||||
|
||||
export function calculateI18nTimeAgoShort(date: string | number | Date | null): {
|
||||
i18n_key: string;
|
||||
time: number;
|
||||
} {
|
||||
if (!date) {
|
||||
return {
|
||||
i18n_key: "",
|
||||
time: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const parsedDate = typeof date === "string" ? parseISO(date) : new Date(date);
|
||||
const now = new Date();
|
||||
const diffInSeconds = (now.getTime() - parsedDate.getTime()) / 1000;
|
||||
|
||||
if (diffInSeconds < 60) {
|
||||
return {
|
||||
i18n_key: "date_time.units_short.s",
|
||||
time: Math.floor(diffInSeconds),
|
||||
};
|
||||
}
|
||||
|
||||
const diffInMinutes = diffInSeconds / 60;
|
||||
if (diffInMinutes < 60) {
|
||||
return {
|
||||
i18n_key: "date_time.units_short.m",
|
||||
time: Math.floor(diffInMinutes),
|
||||
};
|
||||
}
|
||||
|
||||
const diffInHours = diffInMinutes / 60;
|
||||
if (diffInHours < 24) {
|
||||
return {
|
||||
i18n_key: "date_time.units_short.h",
|
||||
time: Math.floor(diffInHours),
|
||||
};
|
||||
}
|
||||
|
||||
const diffInDays = diffInHours / 24;
|
||||
if (diffInDays < 30) {
|
||||
return {
|
||||
i18n_key: "date_time.units_short.d",
|
||||
time: Math.floor(diffInDays),
|
||||
};
|
||||
}
|
||||
|
||||
const diffInMonths = diffInDays / 30;
|
||||
if (diffInMonths < 12) {
|
||||
return {
|
||||
i18n_key: "date_time.units_short.mo",
|
||||
time: Math.floor(diffInMonths),
|
||||
};
|
||||
}
|
||||
|
||||
const diffInYears = diffInMonths / 12;
|
||||
return {
|
||||
i18n_key: "date_time.units_short.y",
|
||||
time: Math.floor(diffInYears),
|
||||
};
|
||||
}
|
||||
|
||||
// Date Validation Helpers
|
||||
/**
|
||||
* @returns {string} boolean value depending on whether the date is greater than today
|
||||
|
||||
Reference in New Issue
Block a user