[WEB-3748, 3749] feat: work item description version history (#6863)
* chore: work item description versions * chore: intake issue description * chore: intake work item description versions * chore: add missing translations * chore: endpoint for intake description version * chore: renamed key to work item * chore: changed the paginator class * chore: authorization added * chore: added the enum validation * chore: removed extra validations * chore: added extra validations * chore: modal position --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
This commit is contained in:
parent
4f68aaafa6
commit
34337f90c1
47 changed files with 978 additions and 102 deletions
|
|
@ -1,7 +1,11 @@
|
|||
from django.urls import path
|
||||
|
||||
|
||||
from plane.app.views import IntakeViewSet, IntakeIssueViewSet
|
||||
from plane.app.views import (
|
||||
IntakeViewSet,
|
||||
IntakeIssueViewSet,
|
||||
IntakeWorkItemDescriptionVersionEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -53,4 +57,14 @@ urlpatterns = [
|
|||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-work-items/<uuid:work_item_id>/description-versions/",
|
||||
IntakeWorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="intake-work-item-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-work-items/<uuid:work_item_id>/description-versions/<uuid:pk>/",
|
||||
IntakeWorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="intake-work-item-versions",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ from plane.app.views import (
|
|||
IssueAttachmentV2Endpoint,
|
||||
IssueBulkUpdateDateEndpoint,
|
||||
IssueVersionEndpoint,
|
||||
IssueDescriptionVersionEndpoint,
|
||||
WorkItemDescriptionVersionEndpoint,
|
||||
IssueMetaEndpoint,
|
||||
IssueDetailIdentifierEndpoint,
|
||||
)
|
||||
|
|
@ -263,22 +263,22 @@ urlpatterns = [
|
|||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
name="issue-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
name="issue-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
|
||||
IssueDescriptionVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:work_item_id>/description-versions/",
|
||||
WorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="work-item-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
|
||||
IssueDescriptionVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:work_item_id>/description-versions/<uuid:pk>/",
|
||||
WorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="work-item-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/meta/",
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ from .issue.sub_issue import SubIssuesEndpoint
|
|||
|
||||
from .issue.subscriber import IssueSubscriberViewSet
|
||||
|
||||
from .issue.version import IssueVersionEndpoint, IssueDescriptionVersionEndpoint
|
||||
from .issue.version import IssueVersionEndpoint, WorkItemDescriptionVersionEndpoint
|
||||
|
||||
from .module.base import (
|
||||
ModuleViewSet,
|
||||
|
|
@ -184,7 +184,11 @@ from .estimate.base import (
|
|||
EstimatePointEndpoint,
|
||||
)
|
||||
|
||||
from .intake.base import IntakeViewSet, IntakeIssueViewSet
|
||||
from .intake.base import (
|
||||
IntakeViewSet,
|
||||
IntakeIssueViewSet,
|
||||
IntakeWorkItemDescriptionVersionEndpoint,
|
||||
)
|
||||
|
||||
from .analytic.base import (
|
||||
AnalyticsEndpoint,
|
||||
|
|
|
|||
|
|
@ -27,16 +27,22 @@ from plane.db.models import (
|
|||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
IssueDescriptionVersion,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
IssueSerializer,
|
||||
IssueDetailSerializer,
|
||||
IntakeSerializer,
|
||||
IntakeIssueSerializer,
|
||||
IntakeIssueDetailSerializer,
|
||||
IssueDescriptionVersionDetailSerializer,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.bgtasks.issue_description_version_task import issue_description_version_task
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from plane.utils.global_paginator import paginate
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
|
|
@ -88,7 +94,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
|||
serializer_class = IntakeIssueSerializer
|
||||
model = IntakeIssue
|
||||
|
||||
filterset_fields = ["statulls"]
|
||||
filterset_fields = ["status"]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
|
|
@ -219,7 +225,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
|||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
|
|
@ -287,6 +293,13 @@ class IntakeIssueViewSet(BaseViewSet):
|
|||
origin=base_host(request=request, is_app=True),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
updated_issue=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
issue_id=str(serializer.data["id"]),
|
||||
user_id=request.user.id,
|
||||
is_creating=True,
|
||||
)
|
||||
intake_issue = (
|
||||
IntakeIssue.objects.select_related("issue")
|
||||
.prefetch_related("issue__labels", "issue__assignees")
|
||||
|
|
@ -386,13 +399,16 @@ class IntakeIssueViewSet(BaseViewSet):
|
|||
),
|
||||
"description": issue_data.get("description", issue.description),
|
||||
}
|
||||
current_instance = json.dumps(
|
||||
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
issue_serializer = IssueCreateSerializer(
|
||||
issue, data=issue_data, partial=True, context={"project_id": project_id}
|
||||
)
|
||||
|
||||
if issue_serializer.is_valid():
|
||||
current_instance = issue
|
||||
|
||||
# Log all the updates
|
||||
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||
if issue is not None:
|
||||
|
|
@ -402,15 +418,18 @@ class IntakeIssueViewSet(BaseViewSet):
|
|||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
updated_issue=current_instance,
|
||||
issue_id=str(pk),
|
||||
user_id=request.user.id,
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
return Response(
|
||||
|
|
@ -550,7 +569,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
|||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
|
|
@ -558,7 +577,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
|||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue = IntakeIssueDetailSerializer(intake_issue).data
|
||||
return Response(issue, status=status.HTTP_200_OK)
|
||||
|
|
@ -585,3 +604,81 @@ class IntakeIssueViewSet(BaseViewSet):
|
|||
|
||||
intake_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class IntakeWorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
|
||||
def process_paginated_result(self, fields, results, timezone):
|
||||
paginated_data = results.values(*fields)
|
||||
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
paginated_data = user_timezone_converter(
|
||||
paginated_data, datetime_fields, timezone
|
||||
)
|
||||
|
||||
return paginated_data
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, work_item_id, pk=None):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=work_item_id
|
||||
)
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue.created_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
if pk:
|
||||
issue_description_version = IssueDescriptionVersion.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=work_item_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
serializer = IssueDescriptionVersionDetailSerializer(
|
||||
issue_description_version
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
cursor = request.GET.get("cursor", None)
|
||||
|
||||
required_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"last_saved_at",
|
||||
"owned_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
|
||||
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=work_item_id
|
||||
)
|
||||
|
||||
paginated_data = paginate(
|
||||
base_queryset=issue_description_versions_queryset,
|
||||
queryset=issue_description_versions_queryset,
|
||||
cursor=cursor,
|
||||
on_result=lambda results: self.process_paginated_result(
|
||||
required_fields, results, request.user.user_timezone
|
||||
),
|
||||
)
|
||||
return Response(paginated_data, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
|
|
@ -565,7 +565,7 @@ class IssueViewSet(BaseViewSet):
|
|||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
recent_visited_task.delay(
|
||||
|
|
@ -632,7 +632,7 @@ class IssueViewSet(BaseViewSet):
|
|||
)
|
||||
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
|
|
@ -1278,7 +1278,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
|
|||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
recent_visited_task.delay(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ from rest_framework import status
|
|||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import IssueVersion, IssueDescriptionVersion
|
||||
from plane.db.models import (
|
||||
IssueVersion,
|
||||
IssueDescriptionVersion,
|
||||
Project,
|
||||
ProjectMember,
|
||||
Issue,
|
||||
)
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
IssueVersionDetailSerializer,
|
||||
|
|
@ -66,7 +72,7 @@ class IssueVersionEndpoint(BaseAPIView):
|
|||
return Response(paginated_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueDescriptionVersionEndpoint(BaseAPIView):
|
||||
class WorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
def process_paginated_result(self, fields, results, timezone):
|
||||
paginated_data = results.values(*fields)
|
||||
|
||||
|
|
@ -78,10 +84,34 @@ class IssueDescriptionVersionEndpoint(BaseAPIView):
|
|||
return paginated_data
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, issue_id, pk=None):
|
||||
def get(self, request, slug, project_id, work_item_id, pk=None):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=work_item_id
|
||||
)
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue.created_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
if pk:
|
||||
issue_description_version = IssueDescriptionVersion.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=work_item_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
serializer = IssueDescriptionVersionDetailSerializer(
|
||||
|
|
@ -105,8 +135,8 @@ class IssueDescriptionVersionEndpoint(BaseAPIView):
|
|||
]
|
||||
|
||||
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id
|
||||
)
|
||||
workspace__slug=slug, project_id=project_id, issue_id=work_item_id
|
||||
).order_by("-created_at")
|
||||
paginated_data = paginate(
|
||||
base_queryset=issue_description_versions_queryset,
|
||||
queryset=issue_description_versions_queryset,
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ class IssueViewViewSet(BaseViewSet):
|
|||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
serializer = IssueViewSerializer(issue_view)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export enum EIssueGroupBYServerToProperty {
|
|||
export enum EIssueServiceType {
|
||||
ISSUES = "issues",
|
||||
EPICS = "epics",
|
||||
WORK_ITEMS = "work-items",
|
||||
}
|
||||
|
||||
export enum EIssuesStoreType {
|
||||
|
|
|
|||
|
|
@ -145,8 +145,8 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
clearEditor: (emitUpdate = false) => {
|
||||
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
setEditorValue: (content: string, emitUpdate = false) => {
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" });
|
||||
},
|
||||
setEditorValueAtCursorPosition: (content: string) => {
|
||||
if (editor?.state.selection) {
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
|||
clearEditor: (emitUpdate = false) => {
|
||||
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
setEditorValue: (content: string, emitUpdate = false) => {
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" });
|
||||
},
|
||||
getMarkDown: (): string => {
|
||||
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export type EditorReadOnlyRefApi = {
|
|||
json: JSONContent | null;
|
||||
};
|
||||
clearEditor: (emitUpdate?: boolean) => void;
|
||||
setEditorValue: (content: string) => void;
|
||||
setEditorValue: (content: string, emitUpdate?: boolean) => void;
|
||||
scrollSummary: (marking: IMarking) => void;
|
||||
getDocumentInfo: () => {
|
||||
characters: number;
|
||||
|
|
|
|||
|
|
@ -2428,5 +2428,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Modul} few {Moduly} other {Modulů}}",
|
||||
"no_module": "Žádný modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Naposledy upraveno uživatelem",
|
||||
"previously_edited_by": "Dříve upraveno uživatelem",
|
||||
"edited_by": "Upraveno uživatelem"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2377,12 +2377,20 @@
|
|||
"manual": "Manuell"
|
||||
}
|
||||
},
|
||||
|
||||
"cycle": {
|
||||
"label": "{count, plural, one {Zyklus} few {Zyklen} other {Zyklen}}",
|
||||
"no_cycle": "Kein Zyklus"
|
||||
},
|
||||
|
||||
"module": {
|
||||
"label": "{count, plural, one {Modul} few {Module} other {Module}}",
|
||||
"no_module": "Kein Modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Zuletzt bearbeitet von",
|
||||
"previously_edited_by": "Zuvor bearbeitet von",
|
||||
"edited_by": "Bearbeitet von"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -630,7 +630,8 @@
|
|||
"clear_sorting": "Clear sorting",
|
||||
"show_weekends": "Show weekends",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable"
|
||||
"disable": "Disable",
|
||||
"copy_markdown": "Copy markdown"
|
||||
},
|
||||
"name": "Name",
|
||||
"discard": "Discard",
|
||||
|
|
@ -2262,5 +2263,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Module} other {Modules}}",
|
||||
"no_module": "No module"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Last edited by",
|
||||
"previously_edited_by": "Previously edited by",
|
||||
"edited_by": "Edited by"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2432,5 +2432,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Módulo} other {Módulos}}",
|
||||
"no_module": "Sin módulo"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Última edición por",
|
||||
"previously_edited_by": "Editado anteriormente por",
|
||||
"edited_by": "Editado por"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2430,5 +2430,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Module} other {Modules}}",
|
||||
"no_module": "Pas de module"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Dernière modification par",
|
||||
"previously_edited_by": "Précédemment modifié par",
|
||||
"edited_by": "Modifié par"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2424,5 +2424,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Modul} other {Modul}}",
|
||||
"no_module": "Tidak ada modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Terakhir disunting oleh",
|
||||
"previously_edited_by": "Sebelumnya disunting oleh",
|
||||
"edited_by": "Disunting oleh"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2429,5 +2429,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Modulo} other {Moduli}}",
|
||||
"no_module": "Nessun modulo"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Ultima modifica di",
|
||||
"previously_edited_by": "Precedentemente modificato da",
|
||||
"edited_by": "Modificato da"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2430,5 +2430,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {モジュール} other {モジュール}}",
|
||||
"no_module": "モジュールなし"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "最終編集者",
|
||||
"previously_edited_by": "以前の編集者",
|
||||
"edited_by": "編集者"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2432,5 +2432,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {모듈} other {모듈}}",
|
||||
"no_module": "모듈 없음"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "마지막 편집자",
|
||||
"previously_edited_by": "이전 편집자",
|
||||
"edited_by": "편집자"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2380,12 +2380,20 @@
|
|||
"manual": "Ręcznie"
|
||||
}
|
||||
},
|
||||
|
||||
"cycle": {
|
||||
"label": "{count, plural, one {Cykl} few {Cykle} other {Cyklów}}",
|
||||
"no_cycle": "Brak cyklu"
|
||||
},
|
||||
|
||||
"module": {
|
||||
"label": "{count, plural, one {Moduł} few {Moduły} other {Modułów}}",
|
||||
"no_module": "Brak modułu"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Ostatnio edytowane przez",
|
||||
"previously_edited_by": "Wcześniej edytowane przez",
|
||||
"edited_by": "Edytowane przez"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2425,5 +2425,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Módulo} other {Módulos}}",
|
||||
"no_module": "Nenhum módulo"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Última edição por",
|
||||
"previously_edited_by": "Anteriormente editado por",
|
||||
"edited_by": "Editado por"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2424,5 +2424,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Modul} other {Module}}",
|
||||
"no_module": "Niciun modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Ultima editare de către",
|
||||
"previously_edited_by": "Editat anterior de către",
|
||||
"edited_by": "Editat de"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2430,5 +2430,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Модуль} other {Модули}}",
|
||||
"no_module": "Нет модуля"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Последнее редактирование",
|
||||
"previously_edited_by": "Ранее отредактировано",
|
||||
"edited_by": "Отредактировано"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2429,5 +2429,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {Modul} few {Moduly} other {Modulov}}",
|
||||
"no_module": "Žiadny modul"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Naposledy upravené používateľom",
|
||||
"previously_edited_by": "Predtým upravené používateľom",
|
||||
"edited_by": "Upravené používateľom"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2379,12 +2379,20 @@
|
|||
"manual": "Вручну"
|
||||
}
|
||||
},
|
||||
|
||||
"cycle": {
|
||||
"label": "{count, plural, one {Цикл} few {Цикли} other {Циклів}}",
|
||||
"no_cycle": "Немає циклу"
|
||||
},
|
||||
|
||||
"module": {
|
||||
"label": "{count, plural, one {Модуль} few {Модулі} other {Модулів}}",
|
||||
"no_module": "Немає модуля"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Останнє редагування",
|
||||
"previously_edited_by": "Раніше відредаговано",
|
||||
"edited_by": "Відредаговано"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2378,12 +2378,20 @@
|
|||
"manual": "Thủ công"
|
||||
}
|
||||
},
|
||||
|
||||
"cycle": {
|
||||
"label": "{count, plural, one {chu kỳ} other {chu kỳ}}",
|
||||
"no_cycle": "Không có chu kỳ"
|
||||
},
|
||||
|
||||
"module": {
|
||||
"label": "{count, plural, one {mô-đun} other {mô-đun}}",
|
||||
"no_module": "Không có mô-đun"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "Chỉnh sửa lần cuối bởi",
|
||||
"previously_edited_by": "Trước đây được chỉnh sửa bởi",
|
||||
"edited_by": "Được chỉnh sửa bởi"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2421,5 +2421,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {模块} other {模块}}",
|
||||
"no_module": "无模块"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "最后编辑者",
|
||||
"previously_edited_by": "之前编辑者",
|
||||
"edited_by": "编辑者"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2432,5 +2432,11 @@
|
|||
"module": {
|
||||
"label": "{count, plural, one {模組} other {模組}}",
|
||||
"no_module": "無模組"
|
||||
},
|
||||
|
||||
"description_versions": {
|
||||
"last_edited_by": "最後編輯者",
|
||||
"previously_edited_by": "先前編輯者",
|
||||
"edited_by": "編輯者"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
packages/types/src/description_version.d.ts
vendored
Normal file
29
packages/types/src/description_version.d.ts
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export type TDescriptionVersion = {
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
id: string;
|
||||
last_saved_at: string;
|
||||
owned_by: string;
|
||||
project: string;
|
||||
updated_at: string;
|
||||
updated_by: string | null;
|
||||
};
|
||||
|
||||
export type TDescriptionVersionDetails = TDescriptionVersion & {
|
||||
description_binary: string | null;
|
||||
description_html: string | null;
|
||||
description_json: object | null;
|
||||
description_stripped: string | null;
|
||||
};
|
||||
|
||||
export type TDescriptionVersionsListResponse = {
|
||||
cursor: string;
|
||||
next_cursor: string | null;
|
||||
next_page_results: boolean;
|
||||
page_count: number;
|
||||
prev_cursor: string | null;
|
||||
prev_page_results: boolean;
|
||||
results: TDescriptionVersion[];
|
||||
total_pages: number;
|
||||
total_results: number;
|
||||
};
|
||||
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
|
|
@ -3,6 +3,7 @@ export * from "./workspace";
|
|||
export * from "./cycle";
|
||||
export * from "./dashboard";
|
||||
export * from "./de-dupe";
|
||||
export * from "./description_version";
|
||||
export * from "./project";
|
||||
export * from "./state";
|
||||
export * from "./issues";
|
||||
|
|
|
|||
2
packages/types/src/issues/issue.d.ts
vendored
2
packages/types/src/issues/issue.d.ts
vendored
|
|
@ -120,7 +120,7 @@ export type TBulkOperationsPayload = {
|
|||
|
||||
export type TIssueDetailWidget = "sub-issues" | "relations" | "links" | "attachments";
|
||||
|
||||
export type TIssueServiceType = EIssueServiceType.ISSUES | EIssueServiceType.EPICS;
|
||||
export type TIssueServiceType = EIssueServiceType.ISSUES | EIssueServiceType.EPICS | EIssueServiceType.WORK_ITEMS;
|
||||
|
||||
export interface IPublicIssue
|
||||
extends Pick<
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { TDescriptionVersion } from "@plane/types";
|
||||
import { Avatar, CustomMenu } from "@plane/ui";
|
||||
import { calculateTimeAgo } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
onClick: (versionId: string) => void;
|
||||
version: TDescriptionVersion;
|
||||
};
|
||||
|
||||
export const DescriptionVersionsDropdownItem: React.FC<Props> = observer((props) => {
|
||||
const { onClick, version } = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
// derived values
|
||||
const versionCreator = version.owned_by ? getUserDetails(version.owned_by) : null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem key={version.id} className="flex items-center gap-1" onClick={() => onClick(version.id)}>
|
||||
<span className="flex-shrink-0">
|
||||
<Avatar name={versionCreator?.display_name} size="sm" src={versionCreator?.avatar_url} />
|
||||
</span>
|
||||
<p className="text-xs text-custom-text-200 flex items-center gap-1.5">
|
||||
<span className="font-medium">{versionCreator?.display_name}</span>
|
||||
<span>{calculateTimeAgo(version.last_saved_at)}</span>
|
||||
</p>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
});
|
||||
59
web/core/components/core/description-versions/dropdown.tsx
Normal file
59
web/core/components/core/description-versions/dropdown.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { History } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TDescriptionVersion } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { calculateTimeAgo } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
// local imports
|
||||
import { DescriptionVersionsDropdownItem } from "./dropdown-item";
|
||||
import { TDescriptionVersionEntityInformation } from "./root";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
entityInformation: TDescriptionVersionEntityInformation;
|
||||
onVersionClick: (versionId: string) => void;
|
||||
versions: TDescriptionVersion[] | undefined;
|
||||
};
|
||||
|
||||
export const DescriptionVersionsDropdown: React.FC<Props> = observer((props) => {
|
||||
const { disabled, entityInformation, onVersionClick, versions } = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
// derived values
|
||||
const latestVersion = versions?.[0];
|
||||
const lastUpdatedAt = latestVersion?.created_at ?? entityInformation.createdAt;
|
||||
const lastUpdatedByUserDetails = getUserDetails(latestVersion?.owned_by ?? entityInformation.createdBy);
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
label={
|
||||
<div className="flex items-center gap-1 text-custom-text-300">
|
||||
<span className="flex-shrink-0 size-4 grid place-items-center">
|
||||
<History className="size-3.5" />
|
||||
</span>
|
||||
<p className="text-xs">
|
||||
{t("description_versions.last_edited_by")}{" "}
|
||||
<span className="font-medium">{lastUpdatedByUserDetails?.display_name}</span>{" "}
|
||||
{calculateTimeAgo(lastUpdatedAt)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
noBorder
|
||||
noChevron={disabled}
|
||||
placement="bottom-end"
|
||||
optionsClassName="w-[300px]"
|
||||
disabled={disabled}
|
||||
closeOnSelect
|
||||
>
|
||||
<p className="text-xs text-custom-text-300 font-medium mb-1">{t("description_versions.previously_edited_by")}</p>
|
||||
{versions?.map((version) => (
|
||||
<DescriptionVersionsDropdownItem key={version.id} onClick={onVersionClick} version={version} />
|
||||
))}
|
||||
</CustomMenu>
|
||||
);
|
||||
});
|
||||
1
web/core/components/core/description-versions/index.ts
Normal file
1
web/core/components/core/description-versions/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./root";
|
||||
194
web/core/components/core/description-versions/modal.tsx
Normal file
194
web/core/components/core/description-versions/modal.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import { useCallback, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronLeft, ChevronRight, Copy } from "lucide-react";
|
||||
// plane imports
|
||||
import { EditorReadOnlyRefApi } from "@plane/editor";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TDescriptionVersion } from "@plane/types";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
EModalPosition,
|
||||
getButtonStyling,
|
||||
Loader,
|
||||
ModalCore,
|
||||
setToast,
|
||||
TOAST_TYPE,
|
||||
Tooltip,
|
||||
} from "@plane/ui";
|
||||
import { calculateTimeAgo, cn, copyTextToClipboard } from "@plane/utils";
|
||||
// components
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
||||
// hooks
|
||||
import { useMember, useWorkspace } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
activeVersionDescription: string | undefined;
|
||||
activeVersionDetails: TDescriptionVersion | undefined;
|
||||
handleClose: () => void;
|
||||
handleNavigation: (direction: "prev" | "next") => void;
|
||||
handleRestore: (descriptionHTML: string) => void;
|
||||
isNextDisabled: boolean;
|
||||
isOpen: boolean;
|
||||
isPrevDisabled: boolean;
|
||||
isRestoreDisabled: boolean;
|
||||
projectId: string | undefined;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const DescriptionVersionsModal: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
activeVersionDescription,
|
||||
activeVersionDetails,
|
||||
handleClose,
|
||||
handleNavigation,
|
||||
handleRestore,
|
||||
isNextDisabled,
|
||||
isPrevDisabled,
|
||||
isOpen,
|
||||
isRestoreDisabled,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
} = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorReadOnlyRefApi>(null);
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const activeVersionId = activeVersionDetails?.id;
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
|
||||
const versionCreator = activeVersionDetails?.owned_by ? getUserDetails(activeVersionDetails.owned_by) : null;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleCopyMarkdown = useCallback(() => {
|
||||
if (!editorRef.current) return;
|
||||
copyTextToClipboard(editorRef.current.getMarkDown()).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("toast.success"),
|
||||
message: "Markdown copied to clipboard.",
|
||||
})
|
||||
);
|
||||
}, [t]);
|
||||
|
||||
if (!workspaceId) return null;
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP}>
|
||||
<div className="p-4" data-prevent-outside-click>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between gap-2 py-0.5">
|
||||
<div className="flex-shrink-0 flex items-center gap-2 text-sm">
|
||||
<p className="flex items-center gap-1">
|
||||
{t("description_versions.edited_by")}
|
||||
<span className="flex-shrink-0">
|
||||
<Avatar size="sm" src={versionCreator?.avatar_url} name={versionCreator?.display_name} />
|
||||
</span>
|
||||
</p>
|
||||
<p className="flex-shrink-0 text-custom-text-200">
|
||||
{calculateTimeAgo(activeVersionDetails?.last_saved_at ?? "")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleNavigation("prev")}
|
||||
className={cn(
|
||||
"size-6 text-custom-text-200 grid place-items-center rounded outline-none transition-colors",
|
||||
{
|
||||
"hover:bg-custom-background-80": !isPrevDisabled,
|
||||
"opacity-50": isPrevDisabled,
|
||||
}
|
||||
)}
|
||||
disabled={isPrevDisabled}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleNavigation("next")}
|
||||
className={cn(
|
||||
"size-6 text-custom-text-200 grid place-items-center rounded outline-none transition-colors",
|
||||
{
|
||||
"hover:bg-custom-background-80": !isNextDisabled,
|
||||
"opacity-50": isNextDisabled,
|
||||
}
|
||||
)}
|
||||
disabled={isNextDisabled}
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* End header */}
|
||||
{/* Version description */}
|
||||
<div className="mt-4 pb-4">
|
||||
{activeVersionDescription ? (
|
||||
<RichTextReadOnlyEditor
|
||||
containerClassName="p-0 !pl-0 border-none"
|
||||
editorClassName="pl-0"
|
||||
id={activeVersionId ?? ""}
|
||||
initialValue={activeVersionDescription ?? "<p></p>"}
|
||||
projectId={projectId}
|
||||
ref={editorRef}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<Loader.Item width="300px" height="15px" />
|
||||
<Loader.Item width="400px" height="15px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="20px" height="15px" />
|
||||
<Loader.Item width="500px" height="15px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="20px" height="15px" />
|
||||
<Loader.Item width="200px" height="15px" />
|
||||
</div>
|
||||
<Loader.Item width="300px" height="15px" />
|
||||
<Loader.Item width="200px" height="15px" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* End version description */}
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between gap-2 pt-4 border-t-[0.5px] border-custom-border-200">
|
||||
<Tooltip tooltipContent={t("common.actions.copy_markdown")}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex-shrink-0",
|
||||
getButtonStyling("neutral-primary", "sm"),
|
||||
"border-none grid place-items-center"
|
||||
)}
|
||||
onClick={handleCopyMarkdown}
|
||||
>
|
||||
<Copy className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
{!isRestoreDisabled && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handleRestore(activeVersionDescription ?? "<p></p>");
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
{t("common.actions.restore")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* End footer */}
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
97
web/core/components/core/description-versions/root.tsx
Normal file
97
web/core/components/core/description-versions/root.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { TDescriptionVersionDetails, TDescriptionVersionsListResponse } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// local imports
|
||||
import { DescriptionVersionsDropdown } from "./dropdown";
|
||||
import { DescriptionVersionsModal } from "./modal";
|
||||
|
||||
export type TDescriptionVersionEntityInformation = {
|
||||
createdAt: Date;
|
||||
createdBy: string;
|
||||
id: string;
|
||||
isRestoreDisabled: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
entityInformation: TDescriptionVersionEntityInformation;
|
||||
fetchHandlers: {
|
||||
listDescriptionVersions: (entityId: string) => Promise<TDescriptionVersionsListResponse>;
|
||||
retrieveDescriptionVersion: (entityId: string, versionId: string) => Promise<TDescriptionVersionDetails>;
|
||||
};
|
||||
handleRestore: (descriptionHTML: string) => void;
|
||||
projectId?: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const DescriptionVersionsRoot: React.FC<Props> = observer((props) => {
|
||||
const { className, entityInformation, fetchHandlers, handleRestore, projectId, workspaceSlug } = props;
|
||||
// states
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [activeVersionId, setActiveVersionId] = useState<string | null>(null);
|
||||
// derived values
|
||||
const entityId = entityInformation.id;
|
||||
// fetch versions list
|
||||
const { data: versionsListResponse } = useSWR(
|
||||
entityId ? `DESCRIPTION_VERSIONS_LIST_${entityId}` : null,
|
||||
entityId ? () => fetchHandlers.listDescriptionVersions(entityId) : null
|
||||
);
|
||||
// fetch active version details
|
||||
const { data: activeVersionResponse } = useSWR(
|
||||
entityId && activeVersionId ? `DESCRIPTION_VERSION_DETAILS_${activeVersionId}` : null,
|
||||
entityId && activeVersionId ? () => fetchHandlers.retrieveDescriptionVersion(entityId, activeVersionId) : null
|
||||
);
|
||||
const versions = versionsListResponse?.results;
|
||||
const versionsCount = versions?.length ?? 0;
|
||||
const activeVersionDetails = versions?.find((version) => version.id === activeVersionId);
|
||||
const activeVersionIndex = versions?.findIndex((version) => version.id === activeVersionId);
|
||||
|
||||
const handleNavigation = useCallback(
|
||||
(direction: "prev" | "next") => {
|
||||
if (activeVersionIndex === undefined) return;
|
||||
if (direction === "prev" && activeVersionIndex > 0) {
|
||||
setActiveVersionId(versions?.[activeVersionIndex - 1].id ?? null);
|
||||
} else if (direction === "next" && activeVersionIndex < versionsCount - 1) {
|
||||
setActiveVersionId(versions?.[activeVersionIndex + 1].id ?? null);
|
||||
}
|
||||
},
|
||||
[activeVersionIndex, versions, versionsCount]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DescriptionVersionsModal
|
||||
activeVersionDescription={activeVersionResponse?.description_html ?? "<p></p>"}
|
||||
activeVersionDetails={activeVersionDetails}
|
||||
handleClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setTimeout(() => {
|
||||
setActiveVersionId(null);
|
||||
}, 300);
|
||||
}}
|
||||
handleNavigation={handleNavigation}
|
||||
handleRestore={handleRestore}
|
||||
isNextDisabled={activeVersionIndex === versionsCount - 1}
|
||||
isOpen={isModalOpen}
|
||||
isPrevDisabled={activeVersionIndex === 0}
|
||||
isRestoreDisabled={entityInformation.isRestoreDisabled}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
<div className={cn(className)}>
|
||||
<DescriptionVersionsDropdown
|
||||
disabled={versionsCount === 0}
|
||||
entityInformation={entityInformation}
|
||||
onVersionClick={(versionId) => {
|
||||
setIsModalOpen(true);
|
||||
setActiveVersionId(versionId);
|
||||
}}
|
||||
versions={versions}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo } from "react";
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// plane types
|
||||
// plane imports
|
||||
import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@plane/constants";
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
import { TIssue, TNameDescriptionLoader } from "@plane/types";
|
||||
// plane ui
|
||||
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { DescriptionVersionsRoot } from "@/components/core/description-versions";
|
||||
import { InboxIssueContentProperties } from "@/components/inbox/content";
|
||||
import {
|
||||
IssueDescriptionInput,
|
||||
|
|
@ -18,7 +19,6 @@ import {
|
|||
TIssueOperations,
|
||||
IssueAttachmentRoot,
|
||||
} from "@/components/issues";
|
||||
// constants
|
||||
// helpers
|
||||
import { getTextContent } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
|
|
@ -27,7 +27,12 @@ import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
|||
// store types
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
// services
|
||||
import { IntakeWorkItemVersionService } from "@/services/inbox";
|
||||
// stores
|
||||
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
||||
// services init
|
||||
const intakeWorkItemVersionService = new IntakeWorkItemVersionService();
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -39,15 +44,20 @@ type Props = {
|
|||
};
|
||||
|
||||
export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
const pathname = usePathname();
|
||||
const { workspaceSlug, projectId, inboxIssue, isEditable, isSubmitting, setIsSubmitting } = props;
|
||||
// hooks
|
||||
// navigation
|
||||
const pathname = usePathname();
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { loader } = useProjectInbox();
|
||||
const { getProjectById } = useProject();
|
||||
const { removeIssue, archiveIssue } = useIssueDetail();
|
||||
// reload confirmation
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
// event tracker
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting === "submitted") {
|
||||
|
|
@ -60,7 +70,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
}
|
||||
}, [isSubmitting, setShowAlert, setIsSubmitting]);
|
||||
|
||||
// dervied values
|
||||
// derived values
|
||||
const issue = inboxIssue.issue;
|
||||
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
|
||||
|
||||
|
|
@ -124,7 +134,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
},
|
||||
path: pathname,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
title: "Work item update failed",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
|
@ -195,6 +205,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
</Loader>
|
||||
) : (
|
||||
<IssueDescriptionInput
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
|
|
@ -207,14 +218,36 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issue.id}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issue.id}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
)}
|
||||
{isEditable && (
|
||||
<DescriptionVersionsRoot
|
||||
className="flex-shrink-0"
|
||||
entityInformation={{
|
||||
createdAt: new Date(issue.created_at ?? ""),
|
||||
createdBy: issue.created_by ?? "",
|
||||
id: issue.id,
|
||||
isRestoreDisabled: !isEditable,
|
||||
}}
|
||||
fetchHandlers={{
|
||||
listDescriptionVersions: (issueId) =>
|
||||
intakeWorkItemVersionService.listDescriptionVersions(workspaceSlug, projectId, issueId),
|
||||
retrieveDescriptionVersion: (issueId, versionId) =>
|
||||
intakeWorkItemVersionService.retrieveDescriptionVersion(workspaceSlug, projectId, issueId, versionId),
|
||||
}}
|
||||
handleRestore={(descriptionHTML) => editorRef.current?.setEditorValue(descriptionHTML, true)}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IssueAttachmentRoot
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ import { FC, useCallback, useEffect, useState } from "react";
|
|||
import debounce from "lodash/debounce";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// i18n
|
||||
// plane imports
|
||||
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { TIssue, TNameDescriptionLoader } from "@plane/types";
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { RichTextEditor, RichTextReadOnlyEditor } from "@/components/editor";
|
||||
|
|
@ -24,6 +23,8 @@ const workspaceService = new WorkspaceService();
|
|||
|
||||
export type IssueDescriptionInputProps = {
|
||||
containerClassName?: string;
|
||||
editorReadOnlyRef?: React.RefObject<EditorReadOnlyRefApi>;
|
||||
editorRef?: React.RefObject<EditorRefApi>;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
|
|
@ -38,6 +39,8 @@ export type IssueDescriptionInputProps = {
|
|||
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
|
||||
const {
|
||||
containerClassName,
|
||||
editorReadOnlyRef,
|
||||
editorRef,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
|
|
@ -55,16 +58,17 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
});
|
||||
// store hooks
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString();
|
||||
// form info
|
||||
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { handleSubmit, reset, control } = useForm<TIssue>({
|
||||
defaultValues: {
|
||||
description_html: initialValue,
|
||||
},
|
||||
});
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
async (formData: Partial<TIssue>) => {
|
||||
|
|
@ -75,10 +79,6 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
[workspaceSlug, projectId, issueId, issueOperations]
|
||||
);
|
||||
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// computed values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string;
|
||||
|
||||
// reset form values
|
||||
useEffect(() => {
|
||||
if (!issueId) return;
|
||||
|
|
@ -102,6 +102,8 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
[handleSubmit, issueId]
|
||||
);
|
||||
|
||||
if (!workspaceId) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{localIssueDescription.description_html ? (
|
||||
|
|
@ -154,6 +156,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
throw new Error("Asset upload failed. Please try again later.");
|
||||
}
|
||||
}}
|
||||
ref={editorRef}
|
||||
/>
|
||||
) : (
|
||||
<RichTextReadOnlyEditor
|
||||
|
|
@ -163,6 +166,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
ref={editorReadOnlyRef}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
import { TNameDescriptionLoader } from "@plane/types";
|
||||
// components
|
||||
import { DescriptionVersionsRoot } from "@/components/core/description-versions";
|
||||
import {
|
||||
IssueActivity,
|
||||
NameDescriptionUpdateStatus,
|
||||
|
|
@ -24,8 +27,12 @@ import useSize from "@/hooks/use-window-size";
|
|||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
// types
|
||||
// services
|
||||
import { WorkItemVersionService } from "@/services/issue";
|
||||
// local imports
|
||||
import { TIssueOperations } from "./root";
|
||||
// services init
|
||||
const workItemVersionService = new WorkItemVersionService();
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -38,6 +45,8 @@ type Props = {
|
|||
|
||||
export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issueOperations, isEditable, isArchived } = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<TNameDescriptionLoader>("saved");
|
||||
// hooks
|
||||
|
|
@ -49,11 +58,9 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
} = useIssueDetail();
|
||||
const { getProjectById } = useProject();
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
|
||||
// derived values
|
||||
const projectDetails = getProjectById(projectId);
|
||||
const issue = issueId ? getIssueById(issueId) : undefined;
|
||||
|
||||
// debounced duplicate issues swr
|
||||
const { duplicateIssues } = useDebouncedDuplicateIssues(
|
||||
workspaceSlug,
|
||||
|
|
@ -114,31 +121,55 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
issueOperations={issueOperations}
|
||||
disabled={!isEditable}
|
||||
disabled={isArchived || !isEditable}
|
||||
value={issue.name}
|
||||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
initialValue={issue.description_html}
|
||||
disabled={!isEditable}
|
||||
disabled={isArchived || !isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 border-none"
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
currentUser={currentUser}
|
||||
disabled={isArchived}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
className="flex-shrink-0"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
currentUser={currentUser}
|
||||
disabled={isArchived}
|
||||
/>
|
||||
)}
|
||||
{isEditable && (
|
||||
<DescriptionVersionsRoot
|
||||
className="flex-shrink-0"
|
||||
entityInformation={{
|
||||
createdAt: new Date(issue.created_at),
|
||||
createdBy: issue.created_by,
|
||||
id: issueId,
|
||||
isRestoreDisabled: !isEditable || isArchived,
|
||||
}}
|
||||
fetchHandlers={{
|
||||
listDescriptionVersions: (issueId) =>
|
||||
workItemVersionService.listDescriptionVersions(workspaceSlug, projectId, issueId),
|
||||
retrieveDescriptionVersion: (issueId, versionId) =>
|
||||
workItemVersionService.retrieveDescriptionVersion(workspaceSlug, projectId, issueId, versionId),
|
||||
}}
|
||||
handleRestore={(descriptionHTML) => editorRef.current?.setEditorValue(descriptionHTML, true)}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IssueDetailWidgets
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
isEditable={!is_archived && isEditable}
|
||||
isEditable={isEditable}
|
||||
isArchived={is_archived}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,30 @@
|
|||
"use-client";
|
||||
import { FC, useEffect } from "react";
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
// plane imports
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
import { TNameDescriptionLoader } from "@plane/types";
|
||||
// components
|
||||
import { DescriptionVersionsRoot } from "@/components/core/description-versions";
|
||||
import { IssueParentDetail, TIssueOperations } from "@/components/issues";
|
||||
// helpers
|
||||
import { getTextContent } from "@/helpers/editor.helper";
|
||||
// store hooks
|
||||
import { useIssueDetail, useProject, useUser } from "@/hooks/store";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject, useUser } from "@/hooks/store";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// plane web components
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
// plane web hooks
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
// services
|
||||
import { WorkItemVersionService } from "@/services/issue";
|
||||
// local components
|
||||
import { IssueDescriptionInput } from "../description-input";
|
||||
import { IssueReaction } from "../issue-detail/reactions";
|
||||
import { IssueTitleInput } from "../title-input";
|
||||
// services init
|
||||
const workItemVersionService = new WorkItemVersionService();
|
||||
|
||||
interface IPeekOverviewIssueDetails {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -33,6 +39,8 @@ interface IPeekOverviewIssueDetails {
|
|||
|
||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer((props) => {
|
||||
const { workspaceSlug, issueId, issueOperations, disabled, isArchived, isSubmitting, setIsSubmitting } = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const {
|
||||
|
|
@ -107,31 +115,63 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isArchived}
|
||||
value={issue.name}
|
||||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
initialValue={issueDescription}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isArchived}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 border-none"
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issueId}
|
||||
currentUser={currentUser}
|
||||
disabled={isArchived}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issueId}
|
||||
currentUser={currentUser}
|
||||
disabled={isArchived}
|
||||
/>
|
||||
)}
|
||||
{!disabled && (
|
||||
<DescriptionVersionsRoot
|
||||
className="flex-shrink-0"
|
||||
entityInformation={{
|
||||
createdAt: new Date(issue.created_at),
|
||||
createdBy: issue.created_by,
|
||||
id: issueId,
|
||||
isRestoreDisabled: disabled || isArchived,
|
||||
}}
|
||||
fetchHandlers={{
|
||||
listDescriptionVersions: (issueId) =>
|
||||
workItemVersionService.listDescriptionVersions(
|
||||
workspaceSlug,
|
||||
issue.project_id?.toString() ?? "",
|
||||
issueId
|
||||
),
|
||||
retrieveDescriptionVersion: (issueId, versionId) =>
|
||||
workItemVersionService.retrieveDescriptionVersion(
|
||||
workspaceSlug,
|
||||
issue.project_id?.toString() ?? "",
|
||||
issueId,
|
||||
versionId
|
||||
),
|
||||
}}
|
||||
handleRestore={(descriptionHTML) => editorRef.current?.setEditorValue(descriptionHTML, true)}
|
||||
projectId={issue.project_id}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled || is_archived || isLocalDBIssueDescription}
|
||||
disabled={disabled || isLocalDBIssueDescription}
|
||||
isArchived={is_archived}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
|
|
@ -226,7 +226,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled || is_archived || isLocalDBIssueDescription}
|
||||
disabled={disabled || isLocalDBIssueDescription}
|
||||
isArchived={is_archived}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from "./inbox-issue.service";
|
||||
export * from "./intake-work_item_version.service";
|
||||
|
|
|
|||
41
web/core/services/inbox/intake-work_item_version.service.ts
Normal file
41
web/core/services/inbox/intake-work_item_version.service.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// plane imports
|
||||
import { type TDescriptionVersionsListResponse, type TDescriptionVersionDetails } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
export class IntakeWorkItemVersionService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async listDescriptionVersions(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
intakeWorkItemId: string
|
||||
): Promise<TDescriptionVersionsListResponse> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-work-items/${intakeWorkItemId}/description-versions/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveDescriptionVersion(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
intakeWorkItemId: string,
|
||||
versionId: string
|
||||
): Promise<TDescriptionVersionDetails> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-work-items/${intakeWorkItemId}/description-versions/${versionId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -7,4 +7,5 @@ export * from "./issue_attachment.service";
|
|||
export * from "./issue_activity.service";
|
||||
export * from "./issue_comment.service";
|
||||
export * from "./issue_relation.service";
|
||||
export * from "./work_item_version.service";
|
||||
export * from "./workspace_draft.service";
|
||||
|
|
|
|||
49
web/core/services/issue/work_item_version.service.ts
Normal file
49
web/core/services/issue/work_item_version.service.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import {
|
||||
type TDescriptionVersionsListResponse,
|
||||
type TDescriptionVersionDetails,
|
||||
type TIssueServiceType,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
export class WorkItemVersionService extends APIService {
|
||||
private serviceType: TIssueServiceType;
|
||||
|
||||
constructor(serviceType: TIssueServiceType = EIssueServiceType.WORK_ITEMS) {
|
||||
super(API_BASE_URL);
|
||||
this.serviceType = serviceType;
|
||||
}
|
||||
|
||||
async listDescriptionVersions(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
workItemId: string
|
||||
): Promise<TDescriptionVersionsListResponse> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${workItemId}/description-versions/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveDescriptionVersion(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
workItemId: string,
|
||||
versionId: string
|
||||
): Promise<TDescriptionVersionDetails> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${workItemId}/description-versions/${versionId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue