This reverts commit e9680cab74.
This commit is contained in:
parent
e9680cab74
commit
9408e92e44
65 changed files with 361 additions and 1469 deletions
|
|
@ -284,11 +284,9 @@ class DraftIssueSerializer(BaseSerializer):
|
||||||
|
|
||||||
class DraftIssueDetailSerializer(DraftIssueSerializer):
|
class DraftIssueDetailSerializer(DraftIssueSerializer):
|
||||||
description_html = serializers.CharField()
|
description_html = serializers.CharField()
|
||||||
description_binary = serializers.CharField()
|
|
||||||
|
|
||||||
class Meta(DraftIssueSerializer.Meta):
|
class Meta(DraftIssueSerializer.Meta):
|
||||||
fields = DraftIssueSerializer.Meta.fields + [
|
fields = DraftIssueSerializer.Meta.fields + [
|
||||||
"description_html",
|
"description_html",
|
||||||
"description_binary",
|
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
# Python imports
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
|
|
@ -735,31 +732,14 @@ class IssueLiteSerializer(DynamicBaseSerializer):
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
class Base64BinaryField(serializers.CharField):
|
|
||||||
def to_representation(self, value):
|
|
||||||
# Encode the binary data to base64 string for JSON response
|
|
||||||
if value:
|
|
||||||
return base64.b64encode(value).decode("utf-8")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
# Decode the base64 string to binary data when saving
|
|
||||||
try:
|
|
||||||
return base64.b64decode(data)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
raise serializers.ValidationError("Invalid base64-encoded data")
|
|
||||||
|
|
||||||
|
|
||||||
class IssueDetailSerializer(IssueSerializer):
|
class IssueDetailSerializer(IssueSerializer):
|
||||||
description_html = serializers.CharField()
|
description_html = serializers.CharField()
|
||||||
description_binary = Base64BinaryField()
|
|
||||||
is_subscribed = serializers.BooleanField(read_only=True)
|
is_subscribed = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta(IssueSerializer.Meta):
|
class Meta(IssueSerializer.Meta):
|
||||||
fields = IssueSerializer.Meta.fields + [
|
fields = IssueSerializer.Meta.fields + [
|
||||||
"description_html",
|
"description_html",
|
||||||
"is_subscribed",
|
"is_subscribed",
|
||||||
"description_binary",
|
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
||||||
|
|
||||||
class WorkspaceMemberMeSerializer(BaseSerializer):
|
class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||||
draft_issue_count = serializers.IntegerField(read_only=True)
|
draft_issue_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceMember
|
model = WorkspaceMember
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,4 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/description/",
|
|
||||||
IntakeIssueViewSet.as_view(
|
|
||||||
{
|
|
||||||
"get": "retrieve_description",
|
|
||||||
"post": "update_description",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="inbox-issue-description",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -66,16 +66,6 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
name="project-issue",
|
name="project-issue",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/description/",
|
|
||||||
IssueViewSet.as_view(
|
|
||||||
{
|
|
||||||
"get": "retrieve_description",
|
|
||||||
"post": "update_description",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="project-issue-description",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||||
LabelViewSet.as_view(
|
LabelViewSet.as_view(
|
||||||
|
|
@ -298,15 +288,6 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
name="project-issue-archive-unarchive",
|
name="project-issue-archive-unarchive",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/description/",
|
|
||||||
IssueArchiveViewSet.as_view(
|
|
||||||
{
|
|
||||||
"get": "retrieve_description",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="archive-issue-description",
|
|
||||||
),
|
|
||||||
## End Issue Archives
|
## End Issue Archives
|
||||||
## Issue Relation
|
## Issue Relation
|
||||||
path(
|
path(
|
||||||
|
|
|
||||||
|
|
@ -276,16 +276,6 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
name="workspace-drafts-issues",
|
name="workspace-drafts-issues",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/draft-issues/<uuid:pk>/description/",
|
|
||||||
WorkspaceDraftIssueViewSet.as_view(
|
|
||||||
{
|
|
||||||
"get": "retrieve_description",
|
|
||||||
"post": "update_description",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="workspace-drafts-issues",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/draft-to-issue/<uuid:draft_id>/",
|
"workspaces/<str:slug>/draft-to-issue/<uuid:draft_id>/",
|
||||||
WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}),
|
WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
# Python imports
|
# Python imports
|
||||||
import json
|
import json
|
||||||
import requests
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Django import
|
# Django import
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
@ -11,9 +9,6 @@ from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db.models import Value, UUIDField
|
from django.db.models import Value, UUIDField
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.http import StreamingHttpResponse
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
@ -45,6 +40,7 @@ from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class IntakeViewSet(BaseViewSet):
|
class IntakeViewSet(BaseViewSet):
|
||||||
|
|
||||||
serializer_class = IntakeSerializer
|
serializer_class = IntakeSerializer
|
||||||
model = Intake
|
model = Intake
|
||||||
|
|
||||||
|
|
@ -93,6 +89,7 @@ class IntakeViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class IntakeIssueViewSet(BaseViewSet):
|
class IntakeIssueViewSet(BaseViewSet):
|
||||||
|
|
||||||
serializer_class = IntakeIssueSerializer
|
serializer_class = IntakeIssueSerializer
|
||||||
model = IntakeIssue
|
model = IntakeIssue
|
||||||
|
|
||||||
|
|
@ -643,82 +640,3 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||||
|
|
||||||
intake_issue.delete()
|
intake_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
|
||||||
def retrieve_description(self, request, slug, project_id, pk):
|
|
||||||
issue = Issue.objects.filter(
|
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id
|
|
||||||
).first()
|
|
||||||
if issue is None:
|
|
||||||
return Response(
|
|
||||||
{"error": "Issue not found"},
|
|
||||||
status=404,
|
|
||||||
)
|
|
||||||
binary_data = issue.description_binary
|
|
||||||
|
|
||||||
def stream_data():
|
|
||||||
if binary_data:
|
|
||||||
yield binary_data
|
|
||||||
else:
|
|
||||||
yield b""
|
|
||||||
|
|
||||||
response = StreamingHttpResponse(
|
|
||||||
stream_data(), content_type="application/octet-stream"
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = (
|
|
||||||
'attachment; filename="issue_description.bin"'
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
|
||||||
def update_description(self, request, slug, project_id, pk):
|
|
||||||
issue = Issue.objects.get(
|
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
|
||||||
)
|
|
||||||
base64_description = issue.description_binary
|
|
||||||
# convert to base64 string
|
|
||||||
if base64_description:
|
|
||||||
base64_description = base64.b64encode(base64_description).decode(
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
data = {
|
|
||||||
"original_document": base64_description,
|
|
||||||
"updates": request.data.get("description_binary"),
|
|
||||||
}
|
|
||||||
base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
|
|
||||||
try:
|
|
||||||
response = requests.post(base_url, json=data, headers=None)
|
|
||||||
except requests.RequestException:
|
|
||||||
return Response(
|
|
||||||
{"error": "Failed to connect to the external service"},
|
|
||||||
status=status.HTTP_502_BAD_GATEWAY,
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
issue.description = response.json().get(
|
|
||||||
"description", issue.description
|
|
||||||
)
|
|
||||||
issue.description_html = response.json().get("description_html")
|
|
||||||
response_description_binary = response.json().get(
|
|
||||||
"description_binary"
|
|
||||||
)
|
|
||||||
issue.description_binary = base64.b64decode(
|
|
||||||
response_description_binary
|
|
||||||
)
|
|
||||||
issue.save()
|
|
||||||
|
|
||||||
def stream_data():
|
|
||||||
if issue.description_binary:
|
|
||||||
yield issue.description_binary
|
|
||||||
else:
|
|
||||||
yield b""
|
|
||||||
|
|
||||||
response = StreamingHttpResponse(
|
|
||||||
stream_data(), content_type="application/octet-stream"
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = (
|
|
||||||
'attachment; filename="issue_description.bin"'
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.http import StreamingHttpResponse
|
|
||||||
|
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
@ -29,7 +27,7 @@ from plane.db.models import (
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
CycleIssue,
|
CycleIssue
|
||||||
)
|
)
|
||||||
from plane.utils.grouper import (
|
from plane.utils.grouper import (
|
||||||
issue_group_values,
|
issue_group_values,
|
||||||
|
|
@ -329,32 +327,6 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
|
||||||
def retrieve_description(self, request, slug, project_id, pk):
|
|
||||||
issue = Issue.objects.filter(
|
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id
|
|
||||||
).first()
|
|
||||||
if issue is None:
|
|
||||||
return Response(
|
|
||||||
{"error": "Issue not found"},
|
|
||||||
status=404,
|
|
||||||
)
|
|
||||||
binary_data = issue.description_binary
|
|
||||||
|
|
||||||
def stream_data():
|
|
||||||
if binary_data:
|
|
||||||
yield binary_data
|
|
||||||
else:
|
|
||||||
yield b""
|
|
||||||
|
|
||||||
response = StreamingHttpResponse(
|
|
||||||
stream_data(), content_type="application/octet-stream"
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = (
|
|
||||||
'attachment; filename="issue_description.bin"'
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class BulkArchiveIssuesEndpoint(BaseAPIView):
|
class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
# Python imports
|
# Python imports
|
||||||
import json
|
import json
|
||||||
import requests
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
|
@ -22,10 +20,8 @@ from django.db.models import (
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.http import StreamingHttpResponse
|
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
@ -729,84 +725,6 @@ class IssueViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
|
||||||
def retrieve_description(self, request, slug, project_id, pk):
|
|
||||||
issue = Issue.issue_objects.filter(
|
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id
|
|
||||||
).first()
|
|
||||||
if issue is None:
|
|
||||||
return Response(
|
|
||||||
{"error": "Issue not found"},
|
|
||||||
status=404,
|
|
||||||
)
|
|
||||||
binary_data = issue.description_binary
|
|
||||||
|
|
||||||
def stream_data():
|
|
||||||
if binary_data:
|
|
||||||
yield binary_data
|
|
||||||
else:
|
|
||||||
yield b""
|
|
||||||
|
|
||||||
response = StreamingHttpResponse(
|
|
||||||
stream_data(), content_type="application/octet-stream"
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = (
|
|
||||||
'attachment; filename="issue_description.bin"'
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def update_description(self, request, slug, project_id, pk):
|
|
||||||
issue = Issue.issue_objects.get(
|
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
|
||||||
)
|
|
||||||
base64_description = issue.description_binary
|
|
||||||
# convert to base64 string
|
|
||||||
if base64_description:
|
|
||||||
base64_description = base64.b64encode(base64_description).decode(
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
data = {
|
|
||||||
"original_document": base64_description,
|
|
||||||
"updates": request.data.get("description_binary"),
|
|
||||||
}
|
|
||||||
base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
|
|
||||||
try:
|
|
||||||
response = requests.post(base_url, json=data, headers=None)
|
|
||||||
except requests.RequestException:
|
|
||||||
return Response(
|
|
||||||
{"error": "Failed to connect to the external service"},
|
|
||||||
status=status.HTTP_502_BAD_GATEWAY,
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
issue.description = response.json().get(
|
|
||||||
"description", issue.description
|
|
||||||
)
|
|
||||||
issue.description_html = response.json().get("description_html")
|
|
||||||
response_description_binary = response.json().get(
|
|
||||||
"description_binary"
|
|
||||||
)
|
|
||||||
issue.description_binary = base64.b64decode(
|
|
||||||
response_description_binary
|
|
||||||
)
|
|
||||||
issue.save()
|
|
||||||
|
|
||||||
def stream_data():
|
|
||||||
if issue.description_binary:
|
|
||||||
yield issue.description_binary
|
|
||||||
else:
|
|
||||||
yield b""
|
|
||||||
|
|
||||||
response = StreamingHttpResponse(
|
|
||||||
stream_data(), content_type="application/octet-stream"
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = (
|
|
||||||
'attachment; filename="issue_description.bin"'
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
|
|
||||||
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
# Python imports
|
# Python imports
|
||||||
import json
|
import json
|
||||||
import requests
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
@ -9,7 +7,6 @@ from django.core import serializers
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.http import StreamingHttpResponse
|
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Q,
|
Q,
|
||||||
UUIDField,
|
UUIDField,
|
||||||
|
|
@ -20,7 +17,6 @@ from django.db.models import (
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
@ -354,78 +350,3 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
|
||||||
def retrieve_description(self, request, slug, pk):
|
|
||||||
issue = DraftIssue.objects.filter(pk=pk, workspace__slug=slug).first()
|
|
||||||
if issue is None:
|
|
||||||
return Response(
|
|
||||||
{"error": "Issue not found"},
|
|
||||||
status=404,
|
|
||||||
)
|
|
||||||
binary_data = issue.description_binary
|
|
||||||
|
|
||||||
def stream_data():
|
|
||||||
if binary_data:
|
|
||||||
yield binary_data
|
|
||||||
else:
|
|
||||||
yield b""
|
|
||||||
|
|
||||||
response = StreamingHttpResponse(
|
|
||||||
stream_data(), content_type="application/octet-stream"
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = (
|
|
||||||
'attachment; filename="draft_issue_description.bin"'
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
|
||||||
def update_description(self, request, slug, pk):
|
|
||||||
issue = DraftIssue.objects.get(workspace__slug=slug, pk=pk)
|
|
||||||
base64_description = issue.description_binary
|
|
||||||
# convert to base64 string
|
|
||||||
if base64_description:
|
|
||||||
base64_description = base64.b64encode(base64_description).decode(
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
data = {
|
|
||||||
"original_document": base64_description,
|
|
||||||
"updates": request.data.get("description_binary"),
|
|
||||||
}
|
|
||||||
base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
|
|
||||||
try:
|
|
||||||
response = requests.post(base_url, json=data, headers=None)
|
|
||||||
except requests.RequestException:
|
|
||||||
return Response(
|
|
||||||
{"error": "Failed to connect to the external service"},
|
|
||||||
status=status.HTTP_502_BAD_GATEWAY,
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
issue.description = response.json().get(
|
|
||||||
"description", issue.description
|
|
||||||
)
|
|
||||||
issue.description_html = response.json().get("description_html")
|
|
||||||
response_description_binary = response.json().get(
|
|
||||||
"description_binary"
|
|
||||||
)
|
|
||||||
issue.description_binary = base64.b64decode(
|
|
||||||
response_description_binary
|
|
||||||
)
|
|
||||||
issue.save()
|
|
||||||
|
|
||||||
def stream_data():
|
|
||||||
if issue.description_binary:
|
|
||||||
yield issue.description_binary
|
|
||||||
else:
|
|
||||||
yield b""
|
|
||||||
|
|
||||||
response = StreamingHttpResponse(
|
|
||||||
stream_data(), content_type="application/octet-stream"
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = (
|
|
||||||
'attachment; filename="issue_description.bin"'
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
# Generated by Django 4.2.15 on 2024-11-06 08:41
|
# Generated by Django 4.2.15 on 2024-11-06 08:41
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,6 @@ CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure"
|
||||||
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
|
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
|
||||||
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
|
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
|
||||||
APP_BASE_URL = os.environ.get("APP_BASE_URL")
|
APP_BASE_URL = os.environ.get("APP_BASE_URL")
|
||||||
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL")
|
|
||||||
|
|
||||||
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
|
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from django.urls import path
|
||||||
|
|
||||||
from plane.space.views import (
|
from plane.space.views import (
|
||||||
IntakeIssuePublicViewSet,
|
IntakeIssuePublicViewSet,
|
||||||
|
IssueVotePublicViewSet,
|
||||||
WorkspaceProjectDeployBoardEndpoint,
|
WorkspaceProjectDeployBoardEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
.next
|
|
||||||
.vercel
|
|
||||||
.tubro
|
|
||||||
out/
|
|
||||||
dist/
|
|
||||||
node_modules/
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 120,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
|
||||||
59
live/src/core/helpers/page.ts
Normal file
59
live/src/core/helpers/page.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { getSchema } from "@tiptap/core";
|
||||||
|
import { generateHTML, generateJSON } from "@tiptap/html";
|
||||||
|
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
|
||||||
|
import * as Y from "yjs"
|
||||||
|
// plane editor
|
||||||
|
import { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps } from "@plane/editor/lib";
|
||||||
|
|
||||||
|
const DOCUMENT_EDITOR_EXTENSIONS = [
|
||||||
|
...CoreEditorExtensionsWithoutProps,
|
||||||
|
...DocumentEditorExtensionsWithoutProps,
|
||||||
|
];
|
||||||
|
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
|
||||||
|
|
||||||
|
export const getAllDocumentFormatsFromBinaryData = (description: Uint8Array): {
|
||||||
|
contentBinaryEncoded: string;
|
||||||
|
contentJSON: object;
|
||||||
|
contentHTML: string;
|
||||||
|
} => {
|
||||||
|
// encode binary description data
|
||||||
|
const base64Data = Buffer.from(description).toString("base64");
|
||||||
|
const yDoc = new Y.Doc();
|
||||||
|
Y.applyUpdate(yDoc, description);
|
||||||
|
// convert to JSON
|
||||||
|
const type = yDoc.getXmlFragment("default");
|
||||||
|
const contentJSON = yXmlFragmentToProseMirrorRootNode(
|
||||||
|
type,
|
||||||
|
documentEditorSchema
|
||||||
|
).toJSON();
|
||||||
|
// convert to HTML
|
||||||
|
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentBinaryEncoded: base64Data,
|
||||||
|
contentJSON,
|
||||||
|
contentHTML,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBinaryDataFromHTMLString = (descriptionHTML: string): {
|
||||||
|
contentBinary: Uint8Array
|
||||||
|
} => {
|
||||||
|
// convert HTML to JSON
|
||||||
|
const contentJSON = generateJSON(
|
||||||
|
descriptionHTML ?? "<p></p>",
|
||||||
|
DOCUMENT_EDITOR_EXTENSIONS
|
||||||
|
);
|
||||||
|
// convert JSON to Y.Doc format
|
||||||
|
const transformedData = prosemirrorJSONToYDoc(
|
||||||
|
documentEditorSchema,
|
||||||
|
contentJSON,
|
||||||
|
"default"
|
||||||
|
);
|
||||||
|
// convert Y.Doc to Uint8Array format
|
||||||
|
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentBinary: encodedData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// plane editor
|
// helpers
|
||||||
import {
|
import {
|
||||||
getAllDocumentFormatsFromDocumentEditorBinaryData,
|
getAllDocumentFormatsFromBinaryData,
|
||||||
getBinaryDataFromDocumentEditorHTMLString,
|
getBinaryDataFromHTMLString,
|
||||||
} from "@plane/editor/lib";
|
} from "@/core/helpers/page.js";
|
||||||
// services
|
// services
|
||||||
import { PageService } from "@/core/services/page.service.js";
|
import { PageService } from "@/core/services/page.service.js";
|
||||||
import { manualLogger } from "../helpers/logger.js";
|
import { manualLogger } from "../helpers/logger.js";
|
||||||
|
|
@ -12,10 +12,12 @@ export const updatePageDescription = async (
|
||||||
params: URLSearchParams,
|
params: URLSearchParams,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
updatedDescription: Uint8Array,
|
updatedDescription: Uint8Array,
|
||||||
cookie: string | undefined
|
cookie: string | undefined,
|
||||||
) => {
|
) => {
|
||||||
if (!(updatedDescription instanceof Uint8Array)) {
|
if (!(updatedDescription instanceof Uint8Array)) {
|
||||||
throw new Error("Invalid updatedDescription: must be an instance of Uint8Array");
|
throw new Error(
|
||||||
|
"Invalid updatedDescription: must be an instance of Uint8Array",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceSlug = params.get("workspaceSlug")?.toString();
|
const workspaceSlug = params.get("workspaceSlug")?.toString();
|
||||||
|
|
@ -23,7 +25,7 @@ export const updatePageDescription = async (
|
||||||
if (!workspaceSlug || !projectId || !cookie) return;
|
if (!workspaceSlug || !projectId || !cookie) return;
|
||||||
|
|
||||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||||
getAllDocumentFormatsFromDocumentEditorBinaryData(updatedDescription);
|
getAllDocumentFormatsFromBinaryData(updatedDescription);
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
description_binary: contentBinaryEncoded,
|
description_binary: contentBinaryEncoded,
|
||||||
|
|
@ -31,7 +33,13 @@ export const updatePageDescription = async (
|
||||||
description: contentJSON,
|
description: contentJSON,
|
||||||
};
|
};
|
||||||
|
|
||||||
await pageService.updateDescription(workspaceSlug, projectId, pageId, payload, cookie);
|
await pageService.updateDescription(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
pageId,
|
||||||
|
payload,
|
||||||
|
cookie,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
manualLogger.error("Update error:", error);
|
manualLogger.error("Update error:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -42,16 +50,26 @@ const fetchDescriptionHTMLAndTransform = async (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
cookie: string
|
cookie: string,
|
||||||
) => {
|
) => {
|
||||||
if (!workspaceSlug || !projectId || !cookie) return;
|
if (!workspaceSlug || !projectId || !cookie) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pageDetails = await pageService.fetchDetails(workspaceSlug, projectId, pageId, cookie);
|
const pageDetails = await pageService.fetchDetails(
|
||||||
const contentBinary = getBinaryDataFromDocumentEditorHTMLString(pageDetails.description_html ?? "<p></p>");
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
pageId,
|
||||||
|
cookie,
|
||||||
|
);
|
||||||
|
const { contentBinary } = getBinaryDataFromHTMLString(
|
||||||
|
pageDetails.description_html ?? "<p></p>",
|
||||||
|
);
|
||||||
return contentBinary;
|
return contentBinary;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
manualLogger.error("Error while transforming from HTML to Uint8Array", error);
|
manualLogger.error(
|
||||||
|
"Error while transforming from HTML to Uint8Array",
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -59,18 +77,28 @@ const fetchDescriptionHTMLAndTransform = async (
|
||||||
export const fetchPageDescriptionBinary = async (
|
export const fetchPageDescriptionBinary = async (
|
||||||
params: URLSearchParams,
|
params: URLSearchParams,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
cookie: string | undefined
|
cookie: string | undefined,
|
||||||
) => {
|
) => {
|
||||||
const workspaceSlug = params.get("workspaceSlug")?.toString();
|
const workspaceSlug = params.get("workspaceSlug")?.toString();
|
||||||
const projectId = params.get("projectId")?.toString();
|
const projectId = params.get("projectId")?.toString();
|
||||||
if (!workspaceSlug || !projectId || !cookie) return null;
|
if (!workspaceSlug || !projectId || !cookie) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await pageService.fetchDescriptionBinary(workspaceSlug, projectId, pageId, cookie);
|
const response = await pageService.fetchDescriptionBinary(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
pageId,
|
||||||
|
cookie,
|
||||||
|
);
|
||||||
const binaryData = new Uint8Array(response);
|
const binaryData = new Uint8Array(response);
|
||||||
|
|
||||||
if (binaryData.byteLength === 0) {
|
if (binaryData.byteLength === 0) {
|
||||||
const binary = await fetchDescriptionHTMLAndTransform(workspaceSlug, projectId, pageId, cookie);
|
const binary = await fetchDescriptionHTMLAndTransform(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
pageId,
|
||||||
|
cookie,
|
||||||
|
);
|
||||||
if (binary) {
|
if (binary) {
|
||||||
return binary;
|
return binary;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
// plane editor
|
|
||||||
import {
|
|
||||||
applyUpdates,
|
|
||||||
convertBase64StringToBinaryData,
|
|
||||||
getAllDocumentFormatsFromRichTextEditorBinaryData,
|
|
||||||
} from "@plane/editor/lib";
|
|
||||||
|
|
||||||
export type TResolveConflictsRequestBody = {
|
|
||||||
original_document: string;
|
|
||||||
updates: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TResolveConflictsResponse = {
|
|
||||||
description_binary: string;
|
|
||||||
description_html: string;
|
|
||||||
description: object;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolveDocumentConflicts = (body: TResolveConflictsRequestBody): TResolveConflictsResponse => {
|
|
||||||
const { original_document, updates } = body;
|
|
||||||
try {
|
|
||||||
// convert from base64 to buffer
|
|
||||||
const originalDocumentBuffer = original_document ? convertBase64StringToBinaryData(original_document) : null;
|
|
||||||
const updatesBuffer = updates ? convertBase64StringToBinaryData(updates) : null;
|
|
||||||
// decode req.body
|
|
||||||
const decodedOriginalDocument = originalDocumentBuffer ? new Uint8Array(originalDocumentBuffer) : new Uint8Array();
|
|
||||||
const decodedUpdates = updatesBuffer ? new Uint8Array(updatesBuffer) : new Uint8Array();
|
|
||||||
// resolve conflicts
|
|
||||||
let resolvedDocument: Uint8Array;
|
|
||||||
if (decodedOriginalDocument.length === 0) {
|
|
||||||
// use updates to create the document id original_description is null
|
|
||||||
resolvedDocument = applyUpdates(decodedUpdates);
|
|
||||||
} else {
|
|
||||||
// use original document and updates to resolve conflicts
|
|
||||||
resolvedDocument = applyUpdates(decodedOriginalDocument, decodedUpdates);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
|
||||||
getAllDocumentFormatsFromRichTextEditorBinaryData(resolvedDocument);
|
|
||||||
|
|
||||||
return {
|
|
||||||
description_binary: contentBinaryEncoded,
|
|
||||||
description_html: contentHTML,
|
|
||||||
description: contentJSON,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Internal server error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -5,13 +5,16 @@ import expressWs from "express-ws";
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
|
|
||||||
|
// cors
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
|
|
||||||
// core hocuspocus server
|
// core hocuspocus server
|
||||||
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
import { errorHandler } from "@/core/helpers/error-handler.js";
|
|
||||||
import { logger, manualLogger } from "@/core/helpers/logger.js";
|
import { logger, manualLogger } from "@/core/helpers/logger.js";
|
||||||
import { resolveDocumentConflicts, TResolveConflictsRequestBody } from "@/core/resolve-conflicts.js";
|
import { errorHandler } from "@/core/helpers/error-handler.js";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
expressWs(app);
|
expressWs(app);
|
||||||
|
|
@ -26,7 +29,7 @@ app.use(
|
||||||
compression({
|
compression({
|
||||||
level: 6,
|
level: 6,
|
||||||
threshold: 5 * 1000,
|
threshold: 5 * 1000,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Logging middleware
|
// Logging middleware
|
||||||
|
|
@ -59,25 +62,6 @@ router.ws("/collaboration", (ws, req) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/resolve-document-conflicts", (req, res) => {
|
|
||||||
const { original_document, updates } = req.body as TResolveConflictsRequestBody;
|
|
||||||
try {
|
|
||||||
if (original_document === undefined || updates === undefined) {
|
|
||||||
res.status(400).send({
|
|
||||||
message: "Missing required fields",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const resolvedDocument = resolveDocumentConflicts(req.body);
|
|
||||||
res.status(200).json(resolvedDocument);
|
|
||||||
} catch (error) {
|
|
||||||
manualLogger.error("Error in /resolve-document-conflicts endpoint:", error);
|
|
||||||
res.status(500).send({
|
|
||||||
message: "Internal server error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(process.env.LIVE_BASE_PATH || "/live", router);
|
app.use(process.env.LIVE_BASE_PATH || "/live", router);
|
||||||
|
|
||||||
app.use((_req, res) => {
|
app.use((_req, res) => {
|
||||||
|
|
@ -98,7 +82,9 @@ const gracefulShutdown = async () => {
|
||||||
try {
|
try {
|
||||||
// Close the HocusPocus server WebSocket connections
|
// Close the HocusPocus server WebSocket connections
|
||||||
await HocusPocusServer.destroy();
|
await HocusPocusServer.destroy();
|
||||||
manualLogger.info("HocusPocus server WebSocket connections closed gracefully.");
|
manualLogger.info(
|
||||||
|
"HocusPocus server WebSocket connections closed gracefully.",
|
||||||
|
);
|
||||||
|
|
||||||
// Close the Express server
|
// Close the Express server
|
||||||
liveServer.close(() => {
|
liveServer.close(() => {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { IssueWidget } from "@/extensions";
|
||||||
// helpers
|
// helpers
|
||||||
import { getEditorClassNames } from "@/helpers/common";
|
import { getEditorClassNames } from "@/helpers/common";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCollaborativeDocumentEditor } from "@/hooks/use-collaborative-document-editor";
|
import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor";
|
||||||
// types
|
// types
|
||||||
import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types";
|
import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types";
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// use document editor
|
// use document editor
|
||||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeDocumentEditor({
|
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
|
||||||
onTransaction,
|
onTransaction,
|
||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
editorClassName,
|
editorClassName,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { IssueWidget } from "@/extensions";
|
||||||
// helpers
|
// helpers
|
||||||
import { getEditorClassNames } from "@/helpers/common";
|
import { getEditorClassNames } from "@/helpers/common";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCollaborativeDocumentReadOnlyEditor } from "@/hooks/use-collaborative-document-read-only-editor";
|
import { useReadOnlyCollaborativeEditor } from "@/hooks/use-read-only-collaborative-editor";
|
||||||
// types
|
// types
|
||||||
import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/types";
|
import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/types";
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeDocumentReadOnlyEditor({
|
const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({
|
||||||
editorClassName,
|
editorClassName,
|
||||||
extensions,
|
extensions,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AnyExtension, Editor } from "@tiptap/core";
|
import { Editor, Extension } from "@tiptap/core";
|
||||||
// components
|
// components
|
||||||
import { EditorContainer } from "@/components/editors";
|
import { EditorContainer } from "@/components/editors";
|
||||||
// constants
|
// constants
|
||||||
|
|
@ -12,7 +12,7 @@ import { EditorContentWrapper } from "./editor-content";
|
||||||
|
|
||||||
type Props = IEditorProps & {
|
type Props = IEditorProps & {
|
||||||
children?: (editor: Editor) => React.ReactNode;
|
children?: (editor: Editor) => React.ReactNode;
|
||||||
extensions: AnyExtension[];
|
extensions: Extension<any, any>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditorWrapper: React.FC<Props> = (props) => {
|
export const EditorWrapper: React.FC<Props> = (props) => {
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
// components
|
|
||||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
|
||||||
import { EditorBubbleMenu } from "@/components/menus";
|
|
||||||
// constants
|
|
||||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
|
||||||
// helpers
|
|
||||||
import { getEditorClassNames } from "@/helpers/common";
|
|
||||||
// hooks
|
|
||||||
import { useCollaborativeRichTextEditor } from "@/hooks/use-collaborative-rich-text-editor";
|
|
||||||
// types
|
|
||||||
import { EditorRefApi, ICollaborativeRichTextEditor } from "@/types";
|
|
||||||
|
|
||||||
const CollaborativeRichTextEditor = (props: ICollaborativeRichTextEditor) => {
|
|
||||||
const {
|
|
||||||
containerClassName,
|
|
||||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
|
||||||
editorClassName,
|
|
||||||
fileHandler,
|
|
||||||
forwardedRef,
|
|
||||||
id,
|
|
||||||
mentionHandler,
|
|
||||||
onChange,
|
|
||||||
placeholder,
|
|
||||||
tabIndex,
|
|
||||||
value,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { editor } = useCollaborativeRichTextEditor({
|
|
||||||
editorClassName,
|
|
||||||
fileHandler,
|
|
||||||
forwardedRef,
|
|
||||||
id,
|
|
||||||
mentionHandler,
|
|
||||||
onChange,
|
|
||||||
placeholder,
|
|
||||||
tabIndex,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const editorContainerClassName = getEditorClassNames({
|
|
||||||
noBorder: true,
|
|
||||||
borderOnFocus: false,
|
|
||||||
containerClassName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!editor) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EditorContainer
|
|
||||||
displayConfig={displayConfig}
|
|
||||||
editor={editor}
|
|
||||||
editorContainerClassName={editorContainerClassName}
|
|
||||||
id={id}
|
|
||||||
>
|
|
||||||
<EditorBubbleMenu editor={editor} />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
|
||||||
</div>
|
|
||||||
</EditorContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CollaborativeRichTextEditorWithRef = React.forwardRef<EditorRefApi, ICollaborativeRichTextEditor>(
|
|
||||||
(props, ref) => (
|
|
||||||
<CollaborativeRichTextEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorRefApi | null>} />
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
CollaborativeRichTextEditorWithRef.displayName = "CollaborativeRichTextEditorWithRef";
|
|
||||||
|
|
||||||
export { CollaborativeRichTextEditorWithRef };
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
// components
|
|
||||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
|
||||||
import { EditorBubbleMenu } from "@/components/menus";
|
|
||||||
// constants
|
|
||||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
|
||||||
// helpers
|
|
||||||
import { getEditorClassNames } from "@/helpers/common";
|
|
||||||
// hooks
|
|
||||||
import { useCollaborativeRichTextReadOnlyEditor } from "@/hooks/use-collaborative-rich-text-read-only-editor";
|
|
||||||
// types
|
|
||||||
import { EditorReadOnlyRefApi, ICollaborativeRichTextReadOnlyEditor } from "@/types";
|
|
||||||
|
|
||||||
const CollaborativeRichTextReadOnlyEditor = (props: ICollaborativeRichTextReadOnlyEditor) => {
|
|
||||||
const {
|
|
||||||
containerClassName,
|
|
||||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
|
||||||
editorClassName,
|
|
||||||
fileHandler,
|
|
||||||
forwardedRef,
|
|
||||||
id,
|
|
||||||
mentionHandler,
|
|
||||||
value,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { editor } = useCollaborativeRichTextReadOnlyEditor({
|
|
||||||
editorClassName,
|
|
||||||
fileHandler,
|
|
||||||
forwardedRef,
|
|
||||||
id,
|
|
||||||
mentionHandler,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const editorContainerClassName = getEditorClassNames({
|
|
||||||
noBorder: true,
|
|
||||||
borderOnFocus: false,
|
|
||||||
containerClassName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!editor) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EditorContainer
|
|
||||||
displayConfig={displayConfig}
|
|
||||||
editor={editor}
|
|
||||||
editorContainerClassName={editorContainerClassName}
|
|
||||||
id={id}
|
|
||||||
>
|
|
||||||
<EditorBubbleMenu editor={editor} />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<EditorContentWrapper editor={editor} id={id} />
|
|
||||||
</div>
|
|
||||||
</EditorContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CollaborativeRichTextReadOnlyEditorWithRef = React.forwardRef<
|
|
||||||
EditorReadOnlyRefApi,
|
|
||||||
ICollaborativeRichTextReadOnlyEditor
|
|
||||||
>((props, ref) => (
|
|
||||||
<CollaborativeRichTextReadOnlyEditor
|
|
||||||
{...props}
|
|
||||||
forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
CollaborativeRichTextReadOnlyEditorWithRef.displayName = "CollaborativeRichTextReadOnlyEditorWithRef";
|
|
||||||
|
|
||||||
export { CollaborativeRichTextReadOnlyEditorWithRef };
|
|
||||||
|
|
@ -1,4 +1,2 @@
|
||||||
export * from "./collaborative-editor";
|
|
||||||
export * from "./collaborative-read-only-editor";
|
|
||||||
export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "./read-only-editor";
|
export * from "./read-only-editor";
|
||||||
|
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
import { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps } from "@/extensions";
|
|
||||||
import { getSchema } from "@tiptap/core";
|
|
||||||
import { generateHTML, generateJSON } from "@tiptap/html";
|
|
||||||
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
|
|
||||||
import * as Y from "yjs";
|
|
||||||
|
|
||||||
// editor extension configs
|
|
||||||
const RICH_TEXT_EDITOR_EXTENSIONS = CoreEditorExtensionsWithoutProps;
|
|
||||||
const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
|
|
||||||
// editor schemas
|
|
||||||
const richTextEditorSchema = getSchema(RICH_TEXT_EDITOR_EXTENSIONS);
|
|
||||||
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description apply updates to a doc and return the updated doc in binary format
|
|
||||||
* @param {Uint8Array} document
|
|
||||||
* @param {Uint8Array} updates
|
|
||||||
* @returns {Uint8Array}
|
|
||||||
*/
|
|
||||||
export const applyUpdates = (document: Uint8Array, updates?: Uint8Array): Uint8Array => {
|
|
||||||
const yDoc = new Y.Doc();
|
|
||||||
Y.applyUpdate(yDoc, document);
|
|
||||||
if (updates) {
|
|
||||||
Y.applyUpdate(yDoc, updates);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
|
|
||||||
return encodedDoc;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description this function encodes binary data to base64 string
|
|
||||||
* @param {Uint8Array} document
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export const convertBinaryDataToBase64String = (document: Uint8Array): string =>
|
|
||||||
Buffer.from(document).toString("base64");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description this function decodes base64 string to binary data
|
|
||||||
* @param {string} document
|
|
||||||
* @returns {ArrayBuffer}
|
|
||||||
*/
|
|
||||||
export const convertBase64StringToBinaryData = (document: string): ArrayBuffer => Buffer.from(document, "base64");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description this function generates the binary equivalent of html content for the rich text editor
|
|
||||||
* @param {string} descriptionHTML
|
|
||||||
* @returns {Uint8Array}
|
|
||||||
*/
|
|
||||||
export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: string): Uint8Array => {
|
|
||||||
// convert HTML to JSON
|
|
||||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", RICH_TEXT_EDITOR_EXTENSIONS);
|
|
||||||
// convert JSON to Y.Doc format
|
|
||||||
const transformedData = prosemirrorJSONToYDoc(richTextEditorSchema, contentJSON, "default");
|
|
||||||
// convert Y.Doc to Uint8Array format
|
|
||||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
|
||||||
return encodedData;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description this function generates the binary equivalent of html content for the document editor
|
|
||||||
* @param {string} descriptionHTML
|
|
||||||
* @returns {Uint8Array}
|
|
||||||
*/
|
|
||||||
export const getBinaryDataFromDocumentEditorHTMLString = (descriptionHTML: string): Uint8Array => {
|
|
||||||
// convert HTML to JSON
|
|
||||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", DOCUMENT_EDITOR_EXTENSIONS);
|
|
||||||
// convert JSON to Y.Doc format
|
|
||||||
const transformedData = prosemirrorJSONToYDoc(documentEditorSchema, contentJSON, "default");
|
|
||||||
// convert Y.Doc to Uint8Array format
|
|
||||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
|
||||||
return encodedData;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description this function generates all document formats for the provided binary data for the rich text editor
|
|
||||||
* @param {Uint8Array} description
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getAllDocumentFormatsFromRichTextEditorBinaryData = (
|
|
||||||
description: Uint8Array
|
|
||||||
): {
|
|
||||||
contentBinaryEncoded: string;
|
|
||||||
contentJSON: object;
|
|
||||||
contentHTML: string;
|
|
||||||
} => {
|
|
||||||
// encode binary description data
|
|
||||||
const base64Data = convertBinaryDataToBase64String(description);
|
|
||||||
const yDoc = new Y.Doc();
|
|
||||||
Y.applyUpdate(yDoc, description);
|
|
||||||
// convert to JSON
|
|
||||||
const type = yDoc.getXmlFragment("default");
|
|
||||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, richTextEditorSchema).toJSON();
|
|
||||||
// convert to HTML
|
|
||||||
const contentHTML = generateHTML(contentJSON, RICH_TEXT_EDITOR_EXTENSIONS);
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentBinaryEncoded: base64Data,
|
|
||||||
contentJSON,
|
|
||||||
contentHTML,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description this function generates all document formats for the provided binary data for the document editor
|
|
||||||
* @param {Uint8Array} description
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getAllDocumentFormatsFromDocumentEditorBinaryData = (
|
|
||||||
description: Uint8Array
|
|
||||||
): {
|
|
||||||
contentBinaryEncoded: string;
|
|
||||||
contentJSON: object;
|
|
||||||
contentHTML: string;
|
|
||||||
} => {
|
|
||||||
// encode binary description data
|
|
||||||
const base64Data = convertBinaryDataToBase64String(description);
|
|
||||||
const yDoc = new Y.Doc();
|
|
||||||
Y.applyUpdate(yDoc, description);
|
|
||||||
// convert to JSON
|
|
||||||
const type = yDoc.getXmlFragment("default");
|
|
||||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, documentEditorSchema).toJSON();
|
|
||||||
// convert to HTML
|
|
||||||
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentBinaryEncoded: base64Data,
|
|
||||||
contentJSON,
|
|
||||||
contentHTML,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
16
packages/editor/src/core/helpers/yjs.ts
Normal file
16
packages/editor/src/core/helpers/yjs.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import * as Y from "yjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description apply updates to a doc and return the updated doc in base64(binary) format
|
||||||
|
* @param {Uint8Array} document
|
||||||
|
* @param {Uint8Array} updates
|
||||||
|
* @returns {string} base64(binary) form of the updated doc
|
||||||
|
*/
|
||||||
|
export const applyUpdates = (document: Uint8Array, updates: Uint8Array): Uint8Array => {
|
||||||
|
const yDoc = new Y.Doc();
|
||||||
|
Y.applyUpdate(yDoc, document);
|
||||||
|
Y.applyUpdate(yDoc, updates);
|
||||||
|
|
||||||
|
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
|
||||||
|
return encodedDoc;
|
||||||
|
};
|
||||||
|
|
@ -9,9 +9,9 @@ import { useEditor } from "@/hooks/use-editor";
|
||||||
// plane editor extensions
|
// plane editor extensions
|
||||||
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||||
// types
|
// types
|
||||||
import { TCollaborativeDocumentEditorHookProps } from "@/types";
|
import { TCollaborativeEditorProps } from "@/types";
|
||||||
|
|
||||||
export const useCollaborativeDocumentEditor = (props: TCollaborativeDocumentEditorHookProps) => {
|
export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
||||||
const {
|
const {
|
||||||
onTransaction,
|
onTransaction,
|
||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
|
|
@ -102,7 +102,7 @@ export const useCollaborativeDocumentEditor = (props: TCollaborativeDocumentEdit
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
placeholder,
|
placeholder,
|
||||||
providerDocument: provider.document,
|
provider,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import Collaboration from "@tiptap/extension-collaboration";
|
|
||||||
import * as Y from "yjs";
|
|
||||||
// extensions
|
|
||||||
import { HeadingListExtension, SideMenuExtension } from "@/extensions";
|
|
||||||
// hooks
|
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
|
||||||
// providers
|
|
||||||
import { CustomCollaborationProvider } from "@/providers";
|
|
||||||
// types
|
|
||||||
import { TCollaborativeRichTextEditorHookProps } from "@/types";
|
|
||||||
|
|
||||||
export const useCollaborativeRichTextEditor = (props: TCollaborativeRichTextEditorHookProps) => {
|
|
||||||
const {
|
|
||||||
editorClassName,
|
|
||||||
editorProps = {},
|
|
||||||
extensions,
|
|
||||||
fileHandler,
|
|
||||||
forwardedRef,
|
|
||||||
handleEditorReady,
|
|
||||||
id,
|
|
||||||
mentionHandler,
|
|
||||||
onChange,
|
|
||||||
placeholder,
|
|
||||||
tabIndex,
|
|
||||||
value,
|
|
||||||
} = props;
|
|
||||||
// initialize custom collaboration provider
|
|
||||||
const provider = useMemo(
|
|
||||||
() =>
|
|
||||||
new CustomCollaborationProvider({
|
|
||||||
name: id,
|
|
||||||
onChange,
|
|
||||||
}),
|
|
||||||
[id]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (provider.hasSynced) return;
|
|
||||||
if (value && value.length > 0) {
|
|
||||||
try {
|
|
||||||
Y.applyUpdate(provider.document, value);
|
|
||||||
provider.hasSynced = true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error applying binary updates to the description", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [value, provider.document]);
|
|
||||||
|
|
||||||
const editor = useEditor({
|
|
||||||
id,
|
|
||||||
editorProps,
|
|
||||||
editorClassName,
|
|
||||||
enableHistory: false,
|
|
||||||
extensions: [
|
|
||||||
SideMenuExtension({
|
|
||||||
aiEnabled: false,
|
|
||||||
dragDropEnabled: true,
|
|
||||||
}),
|
|
||||||
HeadingListExtension,
|
|
||||||
Collaboration.configure({
|
|
||||||
document: provider.document,
|
|
||||||
}),
|
|
||||||
...(extensions ?? []),
|
|
||||||
],
|
|
||||||
fileHandler,
|
|
||||||
handleEditorReady,
|
|
||||||
forwardedRef,
|
|
||||||
mentionHandler,
|
|
||||||
placeholder,
|
|
||||||
providerDocument: provider.document,
|
|
||||||
tabIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
editor,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import Collaboration from "@tiptap/extension-collaboration";
|
|
||||||
import * as Y from "yjs";
|
|
||||||
// extensions
|
|
||||||
import { HeadingListExtension, SideMenuExtension } from "@/extensions";
|
|
||||||
// hooks
|
|
||||||
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
|
||||||
// providers
|
|
||||||
import { CustomCollaborationProvider } from "@/providers";
|
|
||||||
// types
|
|
||||||
import { TCollaborativeRichTextReadOnlyEditorHookProps } from "@/types";
|
|
||||||
|
|
||||||
export const useCollaborativeRichTextReadOnlyEditor = (props: TCollaborativeRichTextReadOnlyEditorHookProps) => {
|
|
||||||
const {
|
|
||||||
editorClassName,
|
|
||||||
editorProps = {},
|
|
||||||
extensions,
|
|
||||||
fileHandler,
|
|
||||||
forwardedRef,
|
|
||||||
handleEditorReady,
|
|
||||||
id,
|
|
||||||
mentionHandler,
|
|
||||||
value,
|
|
||||||
} = props;
|
|
||||||
// initialize custom collaboration provider
|
|
||||||
const provider = useMemo(
|
|
||||||
() =>
|
|
||||||
new CustomCollaborationProvider({
|
|
||||||
name: id,
|
|
||||||
}),
|
|
||||||
[id]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (value.length > 0) {
|
|
||||||
Y.applyUpdate(provider.document, value);
|
|
||||||
}
|
|
||||||
}, [value, provider.document]);
|
|
||||||
|
|
||||||
const editor = useReadOnlyEditor({
|
|
||||||
editorProps,
|
|
||||||
editorClassName,
|
|
||||||
extensions: [
|
|
||||||
SideMenuExtension({
|
|
||||||
aiEnabled: false,
|
|
||||||
dragDropEnabled: true,
|
|
||||||
}),
|
|
||||||
HeadingListExtension,
|
|
||||||
Collaboration.configure({
|
|
||||||
document: provider.document,
|
|
||||||
}),
|
|
||||||
...(extensions ?? []),
|
|
||||||
],
|
|
||||||
fileHandler,
|
|
||||||
handleEditorReady,
|
|
||||||
forwardedRef,
|
|
||||||
mentionHandler,
|
|
||||||
providerDocument: provider.document,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
editor,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
|
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
|
||||||
|
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||||
import { DOMSerializer } from "@tiptap/pm/model";
|
import { DOMSerializer } from "@tiptap/pm/model";
|
||||||
import { Selection } from "@tiptap/pm/state";
|
import { Selection } from "@tiptap/pm/state";
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
|
|
@ -35,7 +36,7 @@ export interface CustomEditorProps {
|
||||||
onTransaction?: () => void;
|
onTransaction?: () => void;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||||
providerDocument?: Y.Doc;
|
provider?: HocuspocusProvider;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
// undefined when prop is not passed, null if intentionally passed to stop
|
// undefined when prop is not passed, null if intentionally passed to stop
|
||||||
// swr syncing
|
// swr syncing
|
||||||
|
|
@ -57,7 +58,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||||
onChange,
|
onChange,
|
||||||
onTransaction,
|
onTransaction,
|
||||||
placeholder,
|
placeholder,
|
||||||
providerDocument,
|
provider,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
value,
|
value,
|
||||||
autofocus = false,
|
autofocus = false,
|
||||||
|
|
@ -205,7 +206,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||||
return markdownOutput;
|
return markdownOutput;
|
||||||
},
|
},
|
||||||
getDocument: () => {
|
getDocument: () => {
|
||||||
const documentBinary = providerDocument ? Y.encodeStateAsUpdate(providerDocument) : null;
|
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
|
||||||
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
||||||
const documentJSON = editorRef.current?.getJSON() ?? null;
|
const documentJSON = editorRef.current?.getJSON() ?? null;
|
||||||
|
|
||||||
|
|
@ -283,7 +284,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||||
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
|
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
|
||||||
}),
|
}),
|
||||||
setProviderDocument: (value) => {
|
setProviderDocument: (value) => {
|
||||||
const document = providerDocument;
|
const document = provider?.document;
|
||||||
if (!document) return;
|
if (!document) return;
|
||||||
Y.applyUpdate(document, value);
|
Y.applyUpdate(document, value);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import { HeadingListExtension } from "@/extensions";
|
||||||
// hooks
|
// hooks
|
||||||
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||||
// types
|
// types
|
||||||
import { TCollaborativeDocumentReadOnlyEditorHookProps } from "@/types";
|
import { TReadOnlyCollaborativeEditorProps } from "@/types";
|
||||||
|
|
||||||
export const useCollaborativeDocumentReadOnlyEditor = (props: TCollaborativeDocumentReadOnlyEditorHookProps) => {
|
export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEditorProps) => {
|
||||||
const {
|
const {
|
||||||
editorClassName,
|
editorClassName,
|
||||||
editorProps = {},
|
editorProps = {},
|
||||||
|
|
@ -79,7 +79,7 @@ export const useCollaborativeDocumentReadOnlyEditor = (props: TCollaborativeDocu
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
handleEditorReady,
|
handleEditorReady,
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
providerDocument: provider.document,
|
provider,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
|
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
|
||||||
|
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
|
|
@ -23,7 +24,7 @@ interface CustomReadOnlyEditorProps {
|
||||||
mentionHandler: {
|
mentionHandler: {
|
||||||
highlights: () => Promise<IMentionHighlight[]>;
|
highlights: () => Promise<IMentionHighlight[]>;
|
||||||
};
|
};
|
||||||
providerDocument?: Y.Doc;
|
provider?: HocuspocusProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||||
|
|
@ -36,7 +37,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||||
fileHandler,
|
fileHandler,
|
||||||
handleEditorReady,
|
handleEditorReady,
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
providerDocument,
|
provider,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const editor = useCustomEditor({
|
const editor = useCustomEditor({
|
||||||
|
|
@ -85,7 +86,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||||
return markdownOutput;
|
return markdownOutput;
|
||||||
},
|
},
|
||||||
getDocument: () => {
|
getDocument: () => {
|
||||||
const documentBinary = providerDocument ? Y.encodeStateAsUpdate(providerDocument) : null;
|
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
|
||||||
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
||||||
const documentJSON = editorRef.current?.getJSON() ?? null;
|
const documentJSON = editorRef.current?.getJSON() ?? null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import * as Y from "yjs";
|
|
||||||
|
|
||||||
export interface CompleteCollaborationProviderConfiguration {
|
|
||||||
/**
|
|
||||||
* The identifier/name of your document
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* The actual Y.js document
|
|
||||||
*/
|
|
||||||
document: Y.Doc;
|
|
||||||
/**
|
|
||||||
* onChange callback
|
|
||||||
*/
|
|
||||||
onChange: (updates: Uint8Array) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CollaborationProviderConfiguration = Required<Pick<CompleteCollaborationProviderConfiguration, "name">> &
|
|
||||||
Partial<CompleteCollaborationProviderConfiguration>;
|
|
||||||
|
|
||||||
export class CustomCollaborationProvider {
|
|
||||||
public hasSynced: boolean;
|
|
||||||
|
|
||||||
public configuration: CompleteCollaborationProviderConfiguration = {
|
|
||||||
name: "",
|
|
||||||
document: new Y.Doc(),
|
|
||||||
onChange: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(configuration: CollaborationProviderConfiguration) {
|
|
||||||
this.hasSynced = false;
|
|
||||||
this.setConfiguration(configuration);
|
|
||||||
this.document.on("update", this.documentUpdateHandler.bind(this));
|
|
||||||
this.document.on("destroy", this.documentDestroyHandler.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
public setConfiguration(configuration: Partial<CompleteCollaborationProviderConfiguration> = {}): void {
|
|
||||||
this.configuration = {
|
|
||||||
...this.configuration,
|
|
||||||
...configuration,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get document() {
|
|
||||||
return this.configuration.document;
|
|
||||||
}
|
|
||||||
|
|
||||||
async documentUpdateHandler(_update: Uint8Array, origin: any) {
|
|
||||||
if (!this.hasSynced) return;
|
|
||||||
// return if the update is from the provider itself
|
|
||||||
if (origin === this) return;
|
|
||||||
// call onChange with the update
|
|
||||||
const stateVector = Y.encodeStateAsUpdate(this.document);
|
|
||||||
this.configuration.onChange?.(stateVector);
|
|
||||||
}
|
|
||||||
|
|
||||||
documentDestroyHandler() {
|
|
||||||
this.document.off("update", this.documentUpdateHandler);
|
|
||||||
this.document.off("destroy", this.documentDestroyHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./custom-collaboration-provider";
|
|
||||||
|
|
@ -19,7 +19,7 @@ export type TServerHandler = {
|
||||||
onServerError?: () => void;
|
onServerError?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TCollaborativeEditorHookCommonProps = {
|
type TCollaborativeEditorHookProps = {
|
||||||
disabledExtensions?: TExtensions[];
|
disabledExtensions?: TExtensions[];
|
||||||
editorClassName: string;
|
editorClassName: string;
|
||||||
editorProps?: EditorProps;
|
editorProps?: EditorProps;
|
||||||
|
|
@ -30,9 +30,12 @@ type TCollaborativeEditorHookCommonProps = {
|
||||||
highlights: () => Promise<IMentionHighlight[]>;
|
highlights: () => Promise<IMentionHighlight[]>;
|
||||||
suggestions?: () => Promise<IMentionSuggestion[]>;
|
suggestions?: () => Promise<IMentionSuggestion[]>;
|
||||||
};
|
};
|
||||||
|
realtimeConfig: TRealtimeConfig;
|
||||||
|
serverHandler?: TServerHandler;
|
||||||
|
user: TUserDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TCollaborativeEditorHookProps = TCollaborativeEditorHookCommonProps & {
|
export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
||||||
onTransaction?: () => void;
|
onTransaction?: () => void;
|
||||||
embedHandler?: TEmbedConfig;
|
embedHandler?: TEmbedConfig;
|
||||||
fileHandler: TFileHandler;
|
fileHandler: TFileHandler;
|
||||||
|
|
@ -41,29 +44,7 @@ type TCollaborativeEditorHookProps = TCollaborativeEditorHookCommonProps & {
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TCollaborativeReadOnlyEditorHookProps = TCollaborativeEditorHookCommonProps & {
|
export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
||||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCollaborativeRichTextEditorHookProps = TCollaborativeEditorHookProps & {
|
|
||||||
onChange: (updatedDescription: Uint8Array) => void;
|
|
||||||
value: Uint8Array;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TCollaborativeRichTextReadOnlyEditorHookProps = TCollaborativeReadOnlyEditorHookProps & {
|
|
||||||
value: Uint8Array;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TCollaborativeDocumentEditorHookProps = TCollaborativeEditorHookProps & {
|
|
||||||
embedHandler?: TEmbedConfig;
|
|
||||||
realtimeConfig: TRealtimeConfig;
|
|
||||||
serverHandler?: TServerHandler;
|
|
||||||
user: TUserDetails;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TCollaborativeDocumentReadOnlyEditorHookProps = TCollaborativeReadOnlyEditorHookProps & {
|
|
||||||
realtimeConfig: TRealtimeConfig;
|
|
||||||
serverHandler?: TServerHandler;
|
|
||||||
user: TUserDetails;
|
|
||||||
};
|
|
||||||
|
|
@ -132,12 +132,6 @@ export interface IRichTextEditor extends IEditorProps {
|
||||||
dragDropEnabled?: boolean;
|
dragDropEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICollaborativeRichTextEditor extends Omit<IEditorProps, "initialValue" | "onChange" | "value"> {
|
|
||||||
dragDropEnabled?: boolean;
|
|
||||||
onChange: (updatedDescription: Uint8Array) => void;
|
|
||||||
value: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICollaborativeDocumentEditor
|
export interface ICollaborativeDocumentEditor
|
||||||
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
|
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
|
||||||
aiHandler?: TAIHandler;
|
aiHandler?: TAIHandler;
|
||||||
|
|
@ -167,10 +161,6 @@ export type ILiteTextReadOnlyEditor = IReadOnlyEditorProps;
|
||||||
|
|
||||||
export type IRichTextReadOnlyEditor = IReadOnlyEditorProps;
|
export type IRichTextReadOnlyEditor = IReadOnlyEditorProps;
|
||||||
|
|
||||||
export type ICollaborativeRichTextReadOnlyEditor = Omit<IReadOnlyEditorProps, "initialValue"> & {
|
|
||||||
value: Uint8Array;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ICollaborativeDocumentReadOnlyEditor extends Omit<IReadOnlyEditorProps, "initialValue"> {
|
export interface ICollaborativeDocumentReadOnlyEditor extends Omit<IReadOnlyEditorProps, "initialValue"> {
|
||||||
embedHandler: TEmbedConfig;
|
embedHandler: TEmbedConfig;
|
||||||
handleEditorReady?: (value: boolean) => void;
|
handleEditorReady?: (value: boolean) => void;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export * from "./ai";
|
export * from "./ai";
|
||||||
export * from "./collaboration-hook";
|
export * from "./collaboration";
|
||||||
export * from "./config";
|
export * from "./config";
|
||||||
export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "./embed";
|
export * from "./embed";
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import "./styles/drag-drop.css";
|
||||||
export {
|
export {
|
||||||
CollaborativeDocumentEditorWithRef,
|
CollaborativeDocumentEditorWithRef,
|
||||||
CollaborativeDocumentReadOnlyEditorWithRef,
|
CollaborativeDocumentReadOnlyEditorWithRef,
|
||||||
CollaborativeRichTextEditorWithRef,
|
|
||||||
CollaborativeRichTextReadOnlyEditorWithRef,
|
|
||||||
DocumentReadOnlyEditorWithRef,
|
DocumentReadOnlyEditorWithRef,
|
||||||
LiteTextEditorWithRef,
|
LiteTextEditorWithRef,
|
||||||
LiteTextReadOnlyEditorWithRef,
|
LiteTextReadOnlyEditorWithRef,
|
||||||
|
|
@ -27,7 +25,7 @@ export * from "@/constants/common";
|
||||||
// helpers
|
// helpers
|
||||||
export * from "@/helpers/common";
|
export * from "@/helpers/common";
|
||||||
export * from "@/helpers/editor-commands";
|
export * from "@/helpers/editor-commands";
|
||||||
export * from "@/helpers/yjs-utils";
|
export * from "@/helpers/yjs";
|
||||||
export * from "@/extensions/table/table";
|
export * from "@/extensions/table/table";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from "@/helpers/yjs-utils";
|
export * from "@/extensions/core-without-props";
|
||||||
|
|
|
||||||
1
packages/types/src/issues/issue.d.ts
vendored
1
packages/types/src/issues/issue.d.ts
vendored
|
|
@ -50,7 +50,6 @@ export type IssueRelation = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIssue = TBaseIssue & {
|
export type TIssue = TBaseIssue & {
|
||||||
description_binary?: string;
|
|
||||||
description_html?: string;
|
description_html?: string;
|
||||||
is_subscribed?: boolean;
|
is_subscribed?: boolean;
|
||||||
parent?: Partial<TBaseIssue>;
|
parent?: Partial<TBaseIssue>;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Popover, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||||
// services
|
// services
|
||||||
import { AIService } from "@/services/ai.service";
|
import { AIService } from "@/services/ai.service";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import React, { forwardRef } from "react";
|
|
||||||
// editor
|
|
||||||
import { CollaborativeRichTextEditorWithRef, EditorRefApi, ICollaborativeRichTextEditor } from "@plane/editor";
|
|
||||||
// types
|
|
||||||
import { IUserLite } from "@plane/types";
|
|
||||||
// helpers
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
|
||||||
// hooks
|
|
||||||
import { useMember, useMention, useUser } from "@/hooks/store";
|
|
||||||
// plane web hooks
|
|
||||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
|
||||||
|
|
||||||
interface Props extends Omit<ICollaborativeRichTextEditor, "fileHandler" | "mentionHandler"> {
|
|
||||||
key: string;
|
|
||||||
projectId: string;
|
|
||||||
uploadFile: (file: File) => Promise<string>;
|
|
||||||
workspaceId: string;
|
|
||||||
workspaceSlug: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CollaborativeRichTextEditor = forwardRef<EditorRefApi, Props>((props, ref) => {
|
|
||||||
const { containerClassName, workspaceSlug, workspaceId, projectId, uploadFile, ...rest } = props;
|
|
||||||
// store hooks
|
|
||||||
const { data: currentUser } = useUser();
|
|
||||||
const {
|
|
||||||
getUserDetails,
|
|
||||||
project: { getProjectMemberIds },
|
|
||||||
} = useMember();
|
|
||||||
// derived values
|
|
||||||
const projectMemberIds = getProjectMemberIds(projectId);
|
|
||||||
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
|
|
||||||
// use-mention
|
|
||||||
const { mentionHighlights, mentionSuggestions } = useMention({
|
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
members: projectMemberDetails,
|
|
||||||
user: currentUser,
|
|
||||||
});
|
|
||||||
// file size
|
|
||||||
const { maxFileSize } = useFileSize();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollaborativeRichTextEditorWithRef
|
|
||||||
ref={ref}
|
|
||||||
fileHandler={getEditorFileHandlers({
|
|
||||||
maxFileSize,
|
|
||||||
projectId,
|
|
||||||
uploadFile,
|
|
||||||
workspaceId,
|
|
||||||
workspaceSlug,
|
|
||||||
})}
|
|
||||||
mentionHandler={{
|
|
||||||
highlights: mentionHighlights,
|
|
||||||
suggestions: mentionSuggestions,
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
containerClassName={cn("relative pl-3", containerClassName)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CollaborativeRichTextEditor.displayName = "CollaborativeRichTextEditor";
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
// editor
|
|
||||||
import {
|
|
||||||
CollaborativeRichTextReadOnlyEditorWithRef,
|
|
||||||
EditorReadOnlyRefApi,
|
|
||||||
ICollaborativeRichTextReadOnlyEditor,
|
|
||||||
} from "@plane/editor";
|
|
||||||
// plane ui
|
|
||||||
import { Loader } from "@plane/ui";
|
|
||||||
// helpers
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
|
||||||
// hooks
|
|
||||||
import { useMention } from "@/hooks/store";
|
|
||||||
import { useIssueDescription } from "@/hooks/use-issue-description";
|
|
||||||
|
|
||||||
type RichTextReadOnlyEditorWrapperProps = Omit<
|
|
||||||
ICollaborativeRichTextReadOnlyEditor,
|
|
||||||
"fileHandler" | "mentionHandler" | "value"
|
|
||||||
> & {
|
|
||||||
descriptionBinary: string | null;
|
|
||||||
descriptionHTML: string;
|
|
||||||
projectId?: string;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CollaborativeRichTextReadOnlyEditor = React.forwardRef<
|
|
||||||
EditorReadOnlyRefApi,
|
|
||||||
RichTextReadOnlyEditorWrapperProps
|
|
||||||
>(({ descriptionBinary: savedDescriptionBinary, descriptionHTML, projectId, workspaceSlug, ...props }, ref) => {
|
|
||||||
const { mentionHighlights } = useMention({});
|
|
||||||
|
|
||||||
const { descriptionBinary } = useIssueDescription({
|
|
||||||
descriptionBinary: savedDescriptionBinary,
|
|
||||||
descriptionHTML,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!descriptionBinary)
|
|
||||||
return (
|
|
||||||
<Loader>
|
|
||||||
<Loader.Item height="150px" />
|
|
||||||
</Loader>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollaborativeRichTextReadOnlyEditorWithRef
|
|
||||||
ref={ref}
|
|
||||||
value={descriptionBinary}
|
|
||||||
fileHandler={getReadOnlyEditorFileHandlers({
|
|
||||||
projectId,
|
|
||||||
workspaceSlug,
|
|
||||||
})}
|
|
||||||
mentionHandler={{
|
|
||||||
highlights: mentionHighlights,
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
// overriding the containerClassName to add relative class passed
|
|
||||||
containerClassName={cn(props.containerClassName, "relative pl-3")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CollaborativeRichTextReadOnlyEditor.displayName = "CollaborativeRichTextReadOnlyEditor";
|
|
||||||
|
|
@ -1,4 +1,2 @@
|
||||||
export * from "./collaborative-editor";
|
export * from "./rich-text-editor";
|
||||||
export * from "./collaborative-read-only-editor";
|
export * from "./rich-text-read-only-editor";
|
||||||
export * from "./editor";
|
|
||||||
export * from "./read-only-editor";
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@
|
||||||
import { Dispatch, SetStateAction, useEffect, useMemo } from "react";
|
import { Dispatch, SetStateAction, useEffect, useMemo } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
// plane types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
// plane ui
|
// plane ui
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { InboxIssueContentProperties } from "@/components/inbox/content";
|
import { InboxIssueContentProperties } from "@/components/inbox/content";
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,12 +22,11 @@ import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@/constants/event-tracker";
|
||||||
// helpers
|
// helpers
|
||||||
import { getTextContent } from "@/helpers/editor.helper";
|
import { getTextContent } from "@/helpers/editor.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useIssueDetail, useProject, useUser } from "@/hooks/store";
|
import { useEventTracker, useIssueDetail, useProject, useProjectInbox, useUser } from "@/hooks/store";
|
||||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||||
// store types
|
// store types
|
||||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||||
// store
|
|
||||||
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -44,6 +45,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||||
const { captureIssueEvent } = useEventTracker();
|
const { captureIssueEvent } = useEventTracker();
|
||||||
|
const { loader } = useProjectInbox();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const { removeIssue, archiveIssue } = useIssueDetail();
|
const { removeIssue, archiveIssue } = useIssueDetail();
|
||||||
|
|
||||||
|
|
@ -58,7 +60,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
}
|
}
|
||||||
}, [isSubmitting, setShowAlert, setIsSubmitting]);
|
}, [isSubmitting, setShowAlert, setIsSubmitting]);
|
||||||
|
|
||||||
// derived values
|
// dervied values
|
||||||
const issue = inboxIssue.issue;
|
const issue = inboxIssue.issue;
|
||||||
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
|
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
|
||||||
|
|
||||||
|
|
@ -73,8 +75,12 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
const issueOperations: TIssueOperations = useMemo(
|
const issueOperations: TIssueOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
fetch: async () => {},
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars, arrow-body-style
|
||||||
remove: async (_workspaceSlug, _projectId, _issueId) => {
|
fetch: async (_workspaceSlug: string, _projectId: string, _issueId: string) => {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars, arrow-body-style
|
||||||
|
remove: async (_workspaceSlug: string, _projectId: string, _issueId: string) => {
|
||||||
try {
|
try {
|
||||||
await removeIssue(workspaceSlug, projectId, _issueId);
|
await removeIssue(workspaceSlug, projectId, _issueId);
|
||||||
setToast({
|
setToast({
|
||||||
|
|
@ -101,7 +107,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update: async (_workspaceSlug, _projectId, _issueId, data) => {
|
update: async (_workspaceSlug: string, _projectId: string, _issueId: string, data: Partial<TIssue>) => {
|
||||||
try {
|
try {
|
||||||
await inboxIssue.updateIssue(data);
|
await inboxIssue.updateIssue(data);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
|
|
@ -113,7 +119,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
},
|
},
|
||||||
path: pathname,
|
path: pathname,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (error) {
|
||||||
setToast({
|
setToast({
|
||||||
title: "Issue update failed",
|
title: "Issue update failed",
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
|
|
@ -130,14 +136,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateDescription: async (_workspaceSlug, _projectId, _issueId, descriptionBinary) => {
|
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
try {
|
|
||||||
return await inboxIssue.updateIssueDescription(descriptionBinary);
|
|
||||||
} catch {
|
|
||||||
throw new Error("Failed to update issue description");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
archive: async (workspaceSlug, projectId, issueId) => {
|
|
||||||
try {
|
try {
|
||||||
await archiveIssue(workspaceSlug, projectId, issueId);
|
await archiveIssue(workspaceSlug, projectId, issueId);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
|
|
@ -155,7 +154,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[archiveIssue, captureIssueEvent, inboxIssue, pathname, projectId, removeIssue, workspaceSlug]
|
[inboxIssue]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!issue?.project_id || !issue?.id) return <></>;
|
if (!issue?.project_id || !issue?.id) return <></>;
|
||||||
|
|
@ -185,20 +184,21 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
containerClassName="-ml-3"
|
containerClassName="-ml-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{issue.description_binary !== undefined && (
|
{loader === "issue-loading" ? (
|
||||||
|
<Loader className="min-h-[6rem] rounded-md border border-custom-border-200">
|
||||||
|
<Loader.Item width="100%" height="140px" />
|
||||||
|
</Loader>
|
||||||
|
) : (
|
||||||
<IssueDescriptionInput
|
<IssueDescriptionInput
|
||||||
key={issue.id}
|
|
||||||
containerClassName="-ml-3 border-none"
|
|
||||||
descriptionBinary={issue.description_binary}
|
|
||||||
descriptionHTML={issue.description_html ?? "<p></p>"}
|
|
||||||
disabled={!isEditable}
|
|
||||||
updateDescription={async (data) =>
|
|
||||||
await issueOperations.updateDescription(workspaceSlug, projectId, issue.id ?? "", data)
|
|
||||||
}
|
|
||||||
issueId={issue.id}
|
|
||||||
projectId={issue.project_id}
|
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={issue.project_id}
|
||||||
|
issueId={issue.id}
|
||||||
|
swrIssueDescription={issue.description_html ?? "<p></p>"}
|
||||||
|
initialValue={issue.description_html ?? "<p></p>"}
|
||||||
|
disabled={!isEditable}
|
||||||
|
issueOperations={issueOperations}
|
||||||
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
|
containerClassName="-ml-3 border-none"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { EFileAssetType } from "@plane/types/src/enums";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { RichTextEditor } from "@/components/editor";
|
import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor";
|
||||||
// constants
|
// constants
|
||||||
import { ETabIndices } from "@/constants/tab-indices";
|
import { ETabIndices } from "@/constants/tab-indices";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
|
||||||
|
|
@ -1,144 +1,157 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, useCallback, useRef } from "react";
|
import { FC, useCallback, useEffect, useState } from "react";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// plane editor
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
|
|
||||||
// types
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
import { EFileAssetType } from "@plane/types/src/enums";
|
import { EFileAssetType } from "@plane/types/src/enums";
|
||||||
// plane ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { CollaborativeRichTextEditor, CollaborativeRichTextReadOnlyEditor } from "@/components/editor";
|
import { RichTextEditor, RichTextReadOnlyEditor } from "@/components/editor";
|
||||||
|
import { TIssueOperations } from "@/components/issues/issue-detail";
|
||||||
// helpers
|
// helpers
|
||||||
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useWorkspace } from "@/hooks/store";
|
import { useWorkspace } from "@/hooks/store";
|
||||||
import { useIssueDescription } from "@/hooks/use-issue-description";
|
|
||||||
// services
|
// services
|
||||||
import { FileService } from "@/services/file.service";
|
import { FileService } from "@/services/file.service";
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
export type IssueDescriptionInputProps = {
|
export type IssueDescriptionInputProps = {
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
descriptionBinary: string | null;
|
|
||||||
descriptionHTML: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
issueId: string;
|
|
||||||
key: string;
|
|
||||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
|
||||||
projectId: string;
|
|
||||||
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
|
|
||||||
updateDescription: (data: string) => Promise<ArrayBuffer>;
|
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
initialValue: string | undefined;
|
||||||
|
disabled?: boolean;
|
||||||
|
issueOperations: TIssueOperations;
|
||||||
|
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||||
|
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
|
||||||
|
swrIssueDescription?: string | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
|
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
containerClassName,
|
containerClassName,
|
||||||
descriptionBinary: savedDescriptionBinary,
|
|
||||||
descriptionHTML,
|
|
||||||
disabled,
|
|
||||||
issueId,
|
|
||||||
placeholder,
|
|
||||||
projectId,
|
|
||||||
setIsSubmitting,
|
|
||||||
updateDescription,
|
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueId,
|
||||||
|
disabled,
|
||||||
|
swrIssueDescription,
|
||||||
|
initialValue,
|
||||||
|
issueOperations,
|
||||||
|
setIsSubmitting,
|
||||||
|
placeholder,
|
||||||
} = props;
|
} = props;
|
||||||
// refs
|
|
||||||
const editorRef = useRef<EditorRefApi>(null);
|
const { handleSubmit, reset, control } = useForm<TIssue>({
|
||||||
// store hooks
|
defaultValues: {
|
||||||
const { getWorkspaceBySlug } = useWorkspace();
|
description_html: initialValue,
|
||||||
// derived values
|
},
|
||||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString() ?? "";
|
|
||||||
// use issue description
|
|
||||||
const { descriptionBinary, resolveConflictsAndUpdateDescription } = useIssueDescription({
|
|
||||||
descriptionBinary: savedDescriptionBinary,
|
|
||||||
descriptionHTML,
|
|
||||||
updateDescription,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const debouncedDescriptionSave = useCallback(
|
const [localIssueDescription, setLocalIssueDescription] = useState({
|
||||||
debounce(async (updatedDescription: Uint8Array) => {
|
id: issueId,
|
||||||
const editor = editorRef.current;
|
description_html: initialValue,
|
||||||
if (!editor) return;
|
});
|
||||||
const encodedDescription = convertBinaryDataToBase64String(updatedDescription);
|
|
||||||
await resolveConflictsAndUpdateDescription(encodedDescription, editor);
|
const handleDescriptionFormSubmit = useCallback(
|
||||||
setIsSubmitting("submitted");
|
async (formData: Partial<TIssue>) => {
|
||||||
}, 1500),
|
await issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||||
[]
|
description_html: formData.description_html ?? "<p></p>",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[workspaceSlug, projectId, issueId, issueOperations]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!descriptionBinary)
|
const { getWorkspaceBySlug } = useWorkspace();
|
||||||
return (
|
// computed values
|
||||||
<Loader className="min-h-[120px] max-h-64 space-y-2 overflow-hidden rounded-md">
|
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string;
|
||||||
<Loader.Item width="100%" height="26px" />
|
|
||||||
<div className="flex items-center gap-2">
|
// reset form values
|
||||||
<Loader.Item width="26px" height="26px" />
|
useEffect(() => {
|
||||||
<Loader.Item width="400px" height="26px" />
|
if (!issueId) return;
|
||||||
</div>
|
reset({
|
||||||
<div className="flex items-center gap-2">
|
id: issueId,
|
||||||
<Loader.Item width="26px" height="26px" />
|
description_html: initialValue === "" ? "<p></p>" : initialValue,
|
||||||
<Loader.Item width="400px" height="26px" />
|
});
|
||||||
</div>
|
setLocalIssueDescription({
|
||||||
<Loader.Item width="80%" height="26px" />
|
id: issueId,
|
||||||
<div className="flex items-center gap-2">
|
description_html: initialValue === "" ? "<p></p>" : initialValue,
|
||||||
<Loader.Item width="50%" height="26px" />
|
});
|
||||||
</div>
|
}, [initialValue, issueId, reset]);
|
||||||
<div className="border-0.5 absolute bottom-2 right-3.5 z-10 flex items-center gap-2">
|
|
||||||
<Loader.Item width="100px" height="26px" />
|
// ADDING handleDescriptionFormSubmit TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS
|
||||||
<Loader.Item width="50px" height="26px" />
|
// TODO: Verify the exhaustive-deps warning
|
||||||
</div>
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
</Loader>
|
const debouncedFormSave = useCallback(
|
||||||
);
|
debounce(async () => {
|
||||||
|
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||||
|
}, 1500),
|
||||||
|
[handleSubmit, issueId]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!disabled ? (
|
{localIssueDescription.description_html ? (
|
||||||
<CollaborativeRichTextEditor
|
<Controller
|
||||||
key={issueId}
|
name="description_html"
|
||||||
containerClassName={containerClassName}
|
control={control}
|
||||||
value={descriptionBinary}
|
render={({ field: { onChange } }) =>
|
||||||
onChange={(val) => {
|
!disabled ? (
|
||||||
setIsSubmitting("submitting");
|
<RichTextEditor
|
||||||
debouncedDescriptionSave(val);
|
id={issueId}
|
||||||
}}
|
initialValue={localIssueDescription.description_html ?? "<p></p>"}
|
||||||
dragDropEnabled
|
value={swrIssueDescription ?? null}
|
||||||
id={issueId}
|
workspaceSlug={workspaceSlug}
|
||||||
placeholder={placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)}
|
workspaceId={workspaceId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
ref={editorRef}
|
dragDropEnabled
|
||||||
uploadFile={async (file) => {
|
onChange={(_description: object, description_html: string) => {
|
||||||
try {
|
setIsSubmitting("submitting");
|
||||||
const { asset_id } = await fileService.uploadProjectAsset(
|
onChange(description_html);
|
||||||
workspaceSlug,
|
debouncedFormSave();
|
||||||
projectId,
|
}}
|
||||||
{
|
placeholder={
|
||||||
entity_identifier: issueId,
|
placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)
|
||||||
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
}
|
||||||
},
|
containerClassName={containerClassName}
|
||||||
file
|
uploadFile={async (file) => {
|
||||||
);
|
try {
|
||||||
return asset_id;
|
const { asset_id } = await fileService.uploadProjectAsset(
|
||||||
} catch (error) {
|
workspaceSlug,
|
||||||
console.log("Error in uploading issue asset:", error);
|
projectId,
|
||||||
throw new Error("Asset upload failed. Please try again later.");
|
{
|
||||||
}
|
entity_identifier: issueId,
|
||||||
}}
|
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
||||||
workspaceId={workspaceId}
|
},
|
||||||
workspaceSlug={workspaceSlug}
|
file
|
||||||
|
);
|
||||||
|
return asset_id;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error in uploading issue asset:", error);
|
||||||
|
throw new Error("Asset upload failed. Please try again later.");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RichTextReadOnlyEditor
|
||||||
|
id={issueId}
|
||||||
|
initialValue={localIssueDescription.description_html ?? ""}
|
||||||
|
containerClassName={containerClassName}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CollaborativeRichTextReadOnlyEditor
|
<Loader>
|
||||||
containerClassName={containerClassName}
|
<Loader.Item height="150px" />
|
||||||
descriptionBinary={savedDescriptionBinary}
|
</Loader>
|
||||||
descriptionHTML={descriptionHTML}
|
|
||||||
id={issueId}
|
|
||||||
projectId={projectId}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import useSize from "@/hooks/use-window-size";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
||||||
// plane web hooks
|
|
||||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||||
// types
|
// types
|
||||||
import { TIssueOperations } from "./root";
|
import { TIssueOperations } from "./root";
|
||||||
|
|
@ -114,22 +113,16 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
containerClassName="-ml-3"
|
containerClassName="-ml-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{issue.description_binary !== undefined && (
|
<IssueDescriptionInput
|
||||||
<IssueDescriptionInput
|
workspaceSlug={workspaceSlug}
|
||||||
key={issue.id}
|
projectId={issue.project_id}
|
||||||
containerClassName="-ml-3 border-none"
|
issueId={issue.id}
|
||||||
descriptionBinary={issue.description_binary}
|
initialValue={issue.description_html}
|
||||||
descriptionHTML={issue.description_html ?? "<p></p>"}
|
disabled={!isEditable}
|
||||||
disabled={!isEditable}
|
issueOperations={issueOperations}
|
||||||
updateDescription={async (data) =>
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
await issueOperations.updateDescription(workspaceSlug, issue.project_id ?? "", issue.id, data)
|
containerClassName="-ml-3 border-none"
|
||||||
}
|
/>
|
||||||
issueId={issue.id}
|
|
||||||
projectId={issue.project_id}
|
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
<IssueReaction
|
<IssueReaction
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,6 @@ import { IssueDetailsSidebar } from "./sidebar";
|
||||||
export type TIssueOperations = {
|
export type TIssueOperations = {
|
||||||
fetch: (workspaceSlug: string, projectId: string, issueId: string, loader?: boolean) => Promise<void>;
|
fetch: (workspaceSlug: string, projectId: string, issueId: string, loader?: boolean) => Promise<void>;
|
||||||
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||||
updateDescription: (
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
issueId: string,
|
|
||||||
descriptionBinary: string
|
|
||||||
) => Promise<ArrayBuffer>;
|
|
||||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
|
|
@ -70,7 +64,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
fetchIssue,
|
fetchIssue,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
updateIssueDescription,
|
|
||||||
removeIssue,
|
removeIssue,
|
||||||
archiveIssue,
|
archiveIssue,
|
||||||
addCycleToIssue,
|
addCycleToIssue,
|
||||||
|
|
@ -125,13 +118,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateDescription: async (workspaceSlug, projectId, issueId, descriptionBinary) => {
|
|
||||||
try {
|
|
||||||
return await updateIssueDescription(workspaceSlug, projectId, issueId, descriptionBinary);
|
|
||||||
} catch {
|
|
||||||
throw new Error("Failed to update issue description");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId);
|
if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId);
|
||||||
|
|
@ -331,7 +317,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||||
is_archived,
|
is_archived,
|
||||||
fetchIssue,
|
fetchIssue,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
updateIssueDescription,
|
|
||||||
removeIssue,
|
removeIssue,
|
||||||
archiveIssue,
|
archiveIssue,
|
||||||
removeArchivedIssue,
|
removeArchivedIssue,
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,8 @@ import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
||||||
// plane web hooks
|
|
||||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
|
||||||
// local components
|
// local components
|
||||||
|
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||||
import { IssueDescriptionInput } from "../description-input";
|
import { IssueDescriptionInput } from "../description-input";
|
||||||
import { IssueReaction } from "../issue-detail/reactions";
|
import { IssueReaction } from "../issue-detail/reactions";
|
||||||
import { IssueTitleInput } from "../title-input";
|
import { IssueTitleInput } from "../title-input";
|
||||||
|
|
@ -64,6 +63,13 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||||
|
|
||||||
if (!issue || !issue.project_id) return <></>;
|
if (!issue || !issue.project_id) return <></>;
|
||||||
|
|
||||||
|
const issueDescription =
|
||||||
|
issue.description_html !== undefined || issue.description_html !== null
|
||||||
|
? issue.description_html != ""
|
||||||
|
? issue.description_html
|
||||||
|
: "<p></p>"
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{issue.parent_id && (
|
{issue.parent_id && (
|
||||||
|
|
@ -99,22 +105,16 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||||
containerClassName="-ml-3"
|
containerClassName="-ml-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{issue.description_binary !== undefined && (
|
<IssueDescriptionInput
|
||||||
<IssueDescriptionInput
|
workspaceSlug={workspaceSlug}
|
||||||
key={issue.id}
|
projectId={issue.project_id}
|
||||||
containerClassName="-ml-3 border-none"
|
issueId={issue.id}
|
||||||
descriptionBinary={issue.description_binary}
|
initialValue={issueDescription}
|
||||||
descriptionHTML={issue.description_html ?? "<p></p>"}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
issueOperations={issueOperations}
|
||||||
updateDescription={async (data) =>
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
await issueOperations.updateDescription(workspaceSlug, issue.project_id ?? "", issue.id, data)
|
containerClassName="-ml-3 border-none"
|
||||||
}
|
/>
|
||||||
issueId={issue.id}
|
|
||||||
projectId={issue.project_id}
|
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
<IssueReaction
|
<IssueReaction
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||||
setPeekIssue,
|
setPeekIssue,
|
||||||
issue: { fetchIssue, getIsFetchingIssueDetails },
|
issue: { fetchIssue, getIsFetchingIssueDetails },
|
||||||
fetchActivities,
|
fetchActivities,
|
||||||
updateIssueDescription,
|
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
|
||||||
const { issues } = useIssuesStore();
|
const { issues } = useIssuesStore();
|
||||||
|
|
@ -93,16 +92,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateDescription: async (workspaceSlug, projectId, issueId, descriptionBinary) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) {
|
|
||||||
throw new Error("Required fields missing while updating binary description");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return await updateIssueDescription(workspaceSlug, projectId, issueId, descriptionBinary);
|
|
||||||
} catch {
|
|
||||||
throw new Error("Failed to update issue description");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
return issues?.removeIssue(workspaceSlug, projectId, issueId).then(() => {
|
return issues?.removeIssue(workspaceSlug, projectId, issueId).then(() => {
|
||||||
|
|
@ -329,17 +318,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[fetchIssue, is_draft, issues, fetchActivities, captureIssueEvent, pathname, removeRoutePeekId, restoreIssue]
|
||||||
fetchIssue,
|
|
||||||
is_draft,
|
|
||||||
issues,
|
|
||||||
fetchActivities,
|
|
||||||
captureIssueEvent,
|
|
||||||
pathname,
|
|
||||||
removeRoutePeekId,
|
|
||||||
restoreIssue,
|
|
||||||
updateIssueDescription,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
||||||
usePageFallback({
|
usePageFallback({
|
||||||
editorRef,
|
editorRef,
|
||||||
fetchPageDescription: async () => {
|
fetchPageDescription: async () => {
|
||||||
if (!page.id) {
|
if (!page.id) return;
|
||||||
throw new Error("Required fields missing while fetching binary description");
|
|
||||||
}
|
|
||||||
return await projectPageService.fetchDescriptionBinary(workspaceSlug, projectId, page.id);
|
return await projectPageService.fetchDescriptionBinary(workspaceSlug, projectId, page.id);
|
||||||
},
|
},
|
||||||
hasConnectionFailed,
|
hasConnectionFailed,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { IUserActivityResponse } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
|
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
|
||||||
// editor
|
// editor
|
||||||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||||
// ui
|
// ui
|
||||||
import { ActivitySettingsLoader } from "@/components/ui";
|
import { ActivitySettingsLoader } from "@/components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import useSWR from "swr";
|
||||||
import { History, MessageSquare } from "lucide-react";
|
import { History, MessageSquare } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
|
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
|
||||||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||||
import { ActivitySettingsLoader } from "@/components/ui";
|
import { ActivitySettingsLoader } from "@/components/ui";
|
||||||
// constants
|
// constants
|
||||||
import { USER_ACTIVITY } from "@/constants/fetch-keys";
|
import { USER_ACTIVITY } from "@/constants/fetch-keys";
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
// plane editor
|
|
||||||
import {
|
|
||||||
convertBase64StringToBinaryData,
|
|
||||||
EditorRefApi,
|
|
||||||
getBinaryDataFromRichTextEditorHTMLString,
|
|
||||||
} from "@plane/editor";
|
|
||||||
|
|
||||||
type TArgs = {
|
|
||||||
descriptionBinary: string | null;
|
|
||||||
descriptionHTML: string | null;
|
|
||||||
updateDescription?: (data: string) => Promise<ArrayBuffer>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useIssueDescription = (args: TArgs) => {
|
|
||||||
const { descriptionBinary: savedDescriptionBinary, descriptionHTML, updateDescription } = args;
|
|
||||||
// states
|
|
||||||
const [descriptionBinary, setDescriptionBinary] = useState<Uint8Array | null>(null);
|
|
||||||
// update description
|
|
||||||
const resolveConflictsAndUpdateDescription = useCallback(
|
|
||||||
async (encodedDescription: string, editorRef: EditorRefApi | null) => {
|
|
||||||
if (!updateDescription) return;
|
|
||||||
try {
|
|
||||||
const conflictFreeEncodedDescription = await updateDescription(encodedDescription);
|
|
||||||
const decodedDescription = conflictFreeEncodedDescription
|
|
||||||
? new Uint8Array(conflictFreeEncodedDescription)
|
|
||||||
: new Uint8Array();
|
|
||||||
editorRef?.setProviderDocument(decodedDescription);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error while updating description", error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[updateDescription]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (descriptionBinary) return;
|
|
||||||
if (savedDescriptionBinary) {
|
|
||||||
const savedDescriptionBuffer = convertBase64StringToBinaryData(savedDescriptionBinary);
|
|
||||||
const decodedSavedDescription = savedDescriptionBuffer
|
|
||||||
? new Uint8Array(savedDescriptionBuffer)
|
|
||||||
: new Uint8Array();
|
|
||||||
setDescriptionBinary(decodedSavedDescription);
|
|
||||||
} else {
|
|
||||||
const decodedDescriptionHTML = getBinaryDataFromRichTextEditorHTMLString(descriptionHTML ?? "<p></p>");
|
|
||||||
setDescriptionBinary(decodedDescriptionHTML);
|
|
||||||
}
|
|
||||||
}, [descriptionBinary, descriptionHTML, savedDescriptionBinary]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
descriptionBinary,
|
|
||||||
resolveConflictsAndUpdateDescription,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
// plane editor
|
// plane editor
|
||||||
import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
|
import { EditorRefApi } from "@plane/editor";
|
||||||
// plane types
|
// plane types
|
||||||
import { TDocumentPayload } from "@plane/types";
|
import { TDocumentPayload } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -8,7 +8,7 @@ import useAutoSave from "@/hooks/use-auto-save";
|
||||||
|
|
||||||
type TArgs = {
|
type TArgs = {
|
||||||
editorRef: React.RefObject<EditorRefApi>;
|
editorRef: React.RefObject<EditorRefApi>;
|
||||||
fetchPageDescription: () => Promise<ArrayBuffer>;
|
fetchPageDescription: () => Promise<any>;
|
||||||
hasConnectionFailed: boolean;
|
hasConnectionFailed: boolean;
|
||||||
updatePageDescription: (data: TDocumentPayload) => Promise<void>;
|
updatePageDescription: (data: TDocumentPayload) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
@ -29,7 +29,7 @@ export const usePageFallback = (args: TArgs) => {
|
||||||
editor.setProviderDocument(latestDecodedDescription);
|
editor.setProviderDocument(latestDecodedDescription);
|
||||||
const { binary, html, json } = editor.getDocument();
|
const { binary, html, json } = editor.getDocument();
|
||||||
if (!binary || !json) return;
|
if (!binary || !json) return;
|
||||||
const encodedBinary = convertBinaryDataToBase64String(binary);
|
const encodedBinary = Buffer.from(binary).toString("base64");
|
||||||
|
|
||||||
await updatePageDescription({
|
await updatePageDescription({
|
||||||
description_binary: encodedBinary,
|
description_binary: encodedBinary,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// types
|
// types
|
||||||
import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm, TDocumentPayload } from "@plane/types";
|
import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm } from "@plane/types";
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
import { APIService } from "@/services/api.service";
|
import { APIService } from "@/services/api.service";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -76,25 +76,6 @@ export class InboxIssueService extends APIService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDescriptionBinary(
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
inboxIssueId: string,
|
|
||||||
data: Pick<TDocumentPayload, "description_binary">
|
|
||||||
): Promise<ArrayBuffer> {
|
|
||||||
return this.post(
|
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inbox-issues/${inboxIssueId}/description/`,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async retrievePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
|
async retrievePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-settings/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-settings/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import isEmpty from "lodash/isEmpty";
|
||||||
import type {
|
import type {
|
||||||
IIssueDisplayProperties,
|
IIssueDisplayProperties,
|
||||||
TBulkOperationsPayload,
|
TBulkOperationsPayload,
|
||||||
TDocumentPayload,
|
|
||||||
TIssue,
|
TIssue,
|
||||||
TIssueActivity,
|
TIssueActivity,
|
||||||
TIssueLink,
|
TIssueLink,
|
||||||
|
|
@ -389,19 +388,4 @@ export class IssueService extends APIService {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDescriptionBinary(
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
issueId: string,
|
|
||||||
data: Pick<TDocumentPayload, "description_binary">
|
|
||||||
): Promise<ArrayBuffer> {
|
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/description/`, data, {
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
})
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,15 @@ import { TDocumentPayload, TPage } from "@plane/types";
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
// services
|
// services
|
||||||
import { APIService } from "@/services/api.service";
|
import { APIService } from "@/services/api.service";
|
||||||
|
import { FileUploadService } from "@/services/file-upload.service";
|
||||||
|
|
||||||
export class ProjectPageService extends APIService {
|
export class ProjectPageService extends APIService {
|
||||||
|
private fileUploadService: FileUploadService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
|
// upload service
|
||||||
|
this.fileUploadService = new FileUploadService();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchAll(workspaceSlug: string, projectId: string): Promise<TPage[]> {
|
async fetchAll(workspaceSlug: string, projectId: string): Promise<TPage[]> {
|
||||||
|
|
@ -128,7 +133,7 @@ export class ProjectPageService extends APIService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise<ArrayBuffer> {
|
async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ export interface IInboxIssueStore {
|
||||||
updateInboxIssueDuplicateTo: (issueId: string) => Promise<void>; // connecting the inbox issue to the project existing issue
|
updateInboxIssueDuplicateTo: (issueId: string) => Promise<void>; // connecting the inbox issue to the project existing issue
|
||||||
updateInboxIssueSnoozeTill: (date: Date | undefined) => Promise<void>; // snooze the issue
|
updateInboxIssueSnoozeTill: (date: Date | undefined) => Promise<void>; // snooze the issue
|
||||||
updateIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
|
updateIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
|
||||||
updateIssueDescription: (descriptionBinary: string) => Promise<ArrayBuffer>; // updating the local issue description
|
|
||||||
updateProjectIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
|
updateProjectIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
|
||||||
fetchIssueActivity: () => Promise<void>; // fetching the issue activity
|
fetchIssueActivity: () => Promise<void>; // fetching the issue activity
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +78,6 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||||
updateInboxIssueDuplicateTo: action,
|
updateInboxIssueDuplicateTo: action,
|
||||||
updateInboxIssueSnoozeTill: action,
|
updateInboxIssueSnoozeTill: action,
|
||||||
updateIssue: action,
|
updateIssue: action,
|
||||||
updateIssueDescription: action,
|
|
||||||
updateProjectIssue: action,
|
updateProjectIssue: action,
|
||||||
fetchIssueActivity: action,
|
fetchIssueActivity: action,
|
||||||
});
|
});
|
||||||
|
|
@ -177,26 +175,6 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateIssueDescription = async (descriptionBinary: string): Promise<ArrayBuffer> => {
|
|
||||||
try {
|
|
||||||
if (!this.issue.id) throw new Error("Issue id is missing");
|
|
||||||
const res = await this.inboxIssueService.updateDescriptionBinary(
|
|
||||||
this.workspaceSlug,
|
|
||||||
this.projectId,
|
|
||||||
this.issue.id,
|
|
||||||
{
|
|
||||||
description_binary: descriptionBinary,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
set(this.issue, "description_binary", descriptionBinary);
|
|
||||||
// fetching activity
|
|
||||||
this.fetchIssueActivity();
|
|
||||||
return res;
|
|
||||||
} catch {
|
|
||||||
throw new Error("Failed to update local issue description");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateProjectIssue = async (issue: Partial<TIssue>) => {
|
updateProjectIssue = async (issue: Partial<TIssue>) => {
|
||||||
const inboxIssue = clone(this.issue);
|
const inboxIssue = clone(this.issue);
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { persistence } from "@/local-db/storage.sqlite";
|
||||||
// services
|
// services
|
||||||
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
|
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
|
||||||
// types
|
// types
|
||||||
import { IIssueRootStore } from "../root.store";
|
|
||||||
import { IIssueDetail } from "./root.store";
|
import { IIssueDetail } from "./root.store";
|
||||||
|
|
||||||
export interface IIssueStoreActions {
|
export interface IIssueStoreActions {
|
||||||
|
|
@ -16,15 +15,9 @@ export interface IIssueStoreActions {
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
issueId: string,
|
issueId: string,
|
||||||
issueStatus?: "DEFAULT" | "DRAFT"
|
issueStatus?: "DEFAULT" | "DRAFT",
|
||||||
) => Promise<TIssue>;
|
) => Promise<TIssue>;
|
||||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||||
updateIssueDescription: (
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
issueId: string,
|
|
||||||
descriptionBinary: string
|
|
||||||
) => Promise<ArrayBuffer>;
|
|
||||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||||
|
|
@ -51,21 +44,19 @@ export class IssueStore implements IIssueStore {
|
||||||
fetchingIssueDetails: string | undefined = undefined;
|
fetchingIssueDetails: string | undefined = undefined;
|
||||||
localDBIssueDescription: string | undefined = undefined;
|
localDBIssueDescription: string | undefined = undefined;
|
||||||
// root store
|
// root store
|
||||||
rootIssueStore: IIssueRootStore;
|
|
||||||
rootIssueDetailStore: IIssueDetail;
|
rootIssueDetailStore: IIssueDetail;
|
||||||
// services
|
// services
|
||||||
issueService;
|
issueService;
|
||||||
issueArchiveService;
|
issueArchiveService;
|
||||||
issueDraftService;
|
issueDraftService;
|
||||||
|
|
||||||
constructor(rootStore: IIssueRootStore, rootIssueDetailStore: IIssueDetail) {
|
constructor(rootStore: IIssueDetail) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
fetchingIssueDetails: observable.ref,
|
fetchingIssueDetails: observable.ref,
|
||||||
localDBIssueDescription: observable.ref,
|
localDBIssueDescription: observable.ref,
|
||||||
});
|
});
|
||||||
// root store
|
// root store
|
||||||
this.rootIssueStore = rootStore;
|
this.rootIssueDetailStore = rootStore;
|
||||||
this.rootIssueDetailStore = rootIssueDetailStore;
|
|
||||||
// services
|
// services
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
this.issueArchiveService = new IssueArchiveService();
|
this.issueArchiveService = new IssueArchiveService();
|
||||||
|
|
@ -165,7 +156,6 @@ export class IssueStore implements IIssueStore {
|
||||||
id: issue?.id,
|
id: issue?.id,
|
||||||
sequence_id: issue?.sequence_id,
|
sequence_id: issue?.sequence_id,
|
||||||
name: issue?.name,
|
name: issue?.name,
|
||||||
description_binary: issue?.description_binary,
|
|
||||||
description_html: issue?.description_html,
|
description_html: issue?.description_html,
|
||||||
sort_order: issue?.sort_order,
|
sort_order: issue?.sort_order,
|
||||||
state_id: issue?.state_id,
|
state_id: issue?.state_id,
|
||||||
|
|
@ -204,20 +194,6 @@ export class IssueStore implements IIssueStore {
|
||||||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateIssueDescription = async (
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
issueId: string,
|
|
||||||
descriptionBinary: string
|
|
||||||
): Promise<ArrayBuffer> => {
|
|
||||||
const res = await this.issueService.updateDescriptionBinary(workspaceSlug, projectId, issueId, {
|
|
||||||
description_binary: descriptionBinary,
|
|
||||||
});
|
|
||||||
this.rootIssueStore.issues.updateIssue(issueId, { description_binary: descriptionBinary });
|
|
||||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||||
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ export class IssueDetail implements IIssueDetail {
|
||||||
|
|
||||||
// store
|
// store
|
||||||
this.rootIssueStore = rootStore;
|
this.rootIssueStore = rootStore;
|
||||||
this.issue = new IssueStore(rootStore, this);
|
this.issue = new IssueStore(this);
|
||||||
this.reaction = new IssueReactionStore(this);
|
this.reaction = new IssueReactionStore(this);
|
||||||
this.attachment = new IssueAttachmentStore(rootStore);
|
this.attachment = new IssueAttachmentStore(rootStore);
|
||||||
this.activity = new IssueActivityStore(rootStore.rootStore as RootStore);
|
this.activity = new IssueActivityStore(rootStore.rootStore as RootStore);
|
||||||
|
|
@ -257,12 +257,6 @@ export class IssueDetail implements IIssueDetail {
|
||||||
) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueStatus);
|
) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueStatus);
|
||||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
||||||
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
updateIssueDescription = async (
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
issueId: string,
|
|
||||||
descriptionBinary: string
|
|
||||||
) => this.issue.updateIssueDescription(workspaceSlug, projectId, issueId, descriptionBinary);
|
|
||||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||||
this.issue.removeIssue(workspaceSlug, projectId, issueId);
|
this.issue.removeIssue(workspaceSlug, projectId, issueId);
|
||||||
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue