From e781ab988073af70d7e0464f616bba22652ed273 Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Thu, 24 Oct 2024 14:26:26 +0530
Subject: [PATCH 01/21] chore: new description binary endpoints
---
apiserver/plane/app/urls/inbox.py | 10 ++++
apiserver/plane/app/urls/issue.py | 20 +++++++
apiserver/plane/app/urls/workspace.py | 10 ++++
apiserver/plane/app/views/inbox/base.py | 55 ++++++++++++++++++++
apiserver/plane/app/views/issue/archive.py | 30 ++++++++++-
apiserver/plane/app/views/issue/base.py | 54 +++++++++++++++++++
apiserver/plane/app/views/workspace/draft.py | 51 ++++++++++++++++++
7 files changed, 229 insertions(+), 1 deletion(-)
diff --git a/apiserver/plane/app/urls/inbox.py b/apiserver/plane/app/urls/inbox.py
index 6508c001d3a..021134d8847 100644
--- a/apiserver/plane/app/urls/inbox.py
+++ b/apiserver/plane/app/urls/inbox.py
@@ -50,4 +50,14 @@
),
name="inbox-issue",
),
+ path(
+ "workspaces//projects//inbox-issues//description/",
+ InboxIssueViewSet.as_view(
+ {
+ "get": "retrieve_description",
+ "post": "update_description",
+ }
+ ),
+ name="inbox-issue-description",
+ ),
]
diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py
index 23330e8e111..53c6f7dd5c5 100644
--- a/apiserver/plane/app/urls/issue.py
+++ b/apiserver/plane/app/urls/issue.py
@@ -58,6 +58,16 @@
),
name="project-issue",
),
+ path(
+ "workspaces//projects//issues//description/",
+ IssueViewSet.as_view(
+ {
+ "get": "retrieve_description",
+ "post": "update_description",
+ }
+ ),
+ name="project-issue-description",
+ ),
path(
"workspaces//projects//issue-labels/",
LabelViewSet.as_view(
@@ -280,6 +290,16 @@
),
name="project-issue-archive-unarchive",
),
+ path(
+ "workspaces//projects//archived-issues//description/",
+ IssueArchiveViewSet.as_view(
+ {
+ "get": "retrieve_description",
+ "post": "update_description",
+ }
+ ),
+ name="archive-issue-description",
+ ),
## End Issue Archives
## Issue Relation
path(
diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py
index fb6f4c13acc..6481f5691cd 100644
--- a/apiserver/plane/app/urls/workspace.py
+++ b/apiserver/plane/app/urls/workspace.py
@@ -276,6 +276,16 @@
),
name="workspace-drafts-issues",
),
+ path(
+ "workspaces//draft-issues//description/",
+ WorkspaceDraftIssueViewSet.as_view(
+ {
+ "get": "retrieve_description",
+ "post": "update_description",
+ }
+ ),
+ name="workspace-drafts-issues",
+ ),
path(
"workspaces//draft-to-issue//",
WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}),
diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py
index 3654df38ea2..7348559ca03 100644
--- a/apiserver/plane/app/views/inbox/base.py
+++ b/apiserver/plane/app/views/inbox/base.py
@@ -1,5 +1,6 @@
# Python imports
import json
+import requests
# Django import
from django.utils import timezone
@@ -9,6 +10,8 @@
from django.contrib.postgres.fields import ArrayField
from django.db.models import Value, UUIDField
from django.db.models.functions import Coalesce
+from django.http import StreamingHttpResponse
+
# Third party imports
from rest_framework import status
@@ -650,3 +653,55 @@ def destroy(self, request, slug, project_id, pk):
inbox_issue.delete()
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
+
+ def update_description(self, request, slug, project_id, pk):
+ issue = Issue.objects.get(
+ workspace__slug=slug, project_id=project_id, pk=pk
+ )
+ data = {
+ "original_document": issue.description_binary,
+ "updates": request.data.get("description_binary"),
+ }
+ scheme = request.scheme
+ host = request.get_host()
+ base_url = f"{scheme}://{host}/resolve-document-conflicts/"
+ response = requests.post(base_url, data=data, headers=None)
+
+ if response.status_code == 200:
+ issue.description = request.data.get(
+ "description", issue.description
+ )
+ issue.description_html = response.json().get("description_html")
+ issue.description_binary = response.json().get(
+ "description_binary"
+ )
+ issue.save()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+ return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py
index 2c6781e59e7..9ebebe44fca 100644
--- a/apiserver/plane/app/views/issue/archive.py
+++ b/apiserver/plane/app/views/issue/archive.py
@@ -7,6 +7,8 @@
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
+from django.http import StreamingHttpResponse
+
# Third Party imports
from rest_framework import status
@@ -27,7 +29,7 @@
IssueLink,
IssueSubscriber,
IssueReaction,
- CycleIssue
+ CycleIssue,
)
from plane.utils.grouper import (
issue_group_values,
@@ -327,6 +329,32 @@ def unarchive(self, request, slug, project_id, pk=None):
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):
permission_classes = [
diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py
index 02a7de9588c..eb4010c5360 100644
--- a/apiserver/plane/app/views/issue/base.py
+++ b/apiserver/plane/app/views/issue/base.py
@@ -1,5 +1,6 @@
# Python imports
import json
+import requests
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
@@ -18,6 +19,7 @@
)
from django.db.models.functions import Coalesce
from django.utils import timezone
+from django.http import StreamingHttpResponse
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
@@ -686,6 +688,58 @@ def destroy(self, request, slug, project_id, pk=None):
)
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
+ )
+ data = {
+ "original_document": issue.description_binary,
+ "updates": request.data.get("description_binary"),
+ }
+ scheme = request.scheme
+ host = request.get_host()
+ base_url = f"{scheme}://{host}/resolve-document-conflicts/"
+ response = requests.post(base_url, data=data, headers=None)
+
+ if response.status_code == 200:
+ issue.description = request.data.get(
+ "description", issue.description
+ )
+ issue.description_html = response.json().get("description_html")
+ issue.description_binary = response.json().get(
+ "description_binary"
+ )
+ issue.save()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+ return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py
index b2cb529fca6..4530d40f7e1 100644
--- a/apiserver/plane/app/views/workspace/draft.py
+++ b/apiserver/plane/app/views/workspace/draft.py
@@ -1,5 +1,6 @@
# Python imports
import json
+import requests
# Django imports
from django.utils import timezone
@@ -7,6 +8,7 @@
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
+from django.http import StreamingHttpResponse
from django.db.models import (
Q,
UUIDField,
@@ -350,3 +352,52 @@ def create_draft_to_issue(self, request, slug, draft_id):
return Response(serializer.data, status=status.HTTP_201_CREATED)
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)
+ data = {
+ "original_document": issue.description_binary,
+ "updates": request.data.get("description_binary"),
+ }
+ scheme = request.scheme
+ host = request.get_host()
+ base_url = f"{scheme}://{host}/resolve-document-conflicts/"
+ response = requests.post(base_url, data=data, headers=None)
+
+ if response.status_code == 200:
+ issue.description = request.data.get(
+ "description", issue.description
+ )
+ issue.description_html = response.json().get("description_html")
+ issue.description_binary = response.json().get(
+ "description_binary"
+ )
+ issue.save()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+ return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
From cccc7b7d490ca759cedc280b19f1212bf8e79523 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Thu, 24 Oct 2024 22:02:15 +0530
Subject: [PATCH 02/21] chore: conflict free issue description
---
apiserver/plane/app/views/inbox/base.py | 40 +++-
apiserver/plane/app/views/issue/base.py | 38 +++-
apiserver/plane/app/views/workspace/draft.py | 36 +++-
apiserver/plane/settings/common.py | 1 +
live/.prettierignore | 6 +
live/.prettierrc | 5 +
live/src/core/helpers/document.ts | 16 ++
live/src/core/helpers/issue.ts | 33 ++++
live/src/core/helpers/page.ts | 41 ++--
live/src/core/lib/page.ts | 54 ++----
live/src/server.ts | 56 +++++-
.../editors/document/collaborative-editor.tsx | 4 +-
.../collaborative-read-only-editor.tsx | 4 +-
.../components/editors/editor-wrapper.tsx | 4 +-
.../rich-text/collaborative-editor.tsx | 72 +++++++
.../collaborative-read-only-editor.tsx | 70 +++++++
.../components/editors/rich-text/index.ts | 2 +
packages/editor/src/core/helpers/yjs.ts | 16 ++
...s => use-collaborative-document-editor.ts} | 6 +-
...ollaborative-document-read-only-editor.ts} | 6 +-
.../use-collaborative-rich-text-editor.ts | 72 +++++++
...ollaborative-rich-text-read-only-editor.ts | 64 +++++++
packages/editor/src/core/hooks/use-editor.ts | 9 +-
.../src/core/hooks/use-read-only-editor.ts | 7 +-
.../custom-collaboration-provider.ts | 57 ++++++
packages/editor/src/core/providers/index.ts | 1 +
...collaboration.ts => collaboration-hook.ts} | 32 +++-
packages/editor/src/core/types/editor.ts | 10 +
packages/editor/src/core/types/index.ts | 2 +-
packages/editor/src/index.ts | 2 +
.../core/modals/gpt-assistant-popover.tsx | 2 +-
.../rich-text-editor/collaborative-editor.tsx | 107 +++++++++++
.../collaborative-read-only-editor.tsx | 63 ++++++
.../{rich-text-editor.tsx => editor.tsx} | 0
.../editor/rich-text-editor/index.ts | 6 +-
...d-only-editor.tsx => read-only-editor.tsx} | 0
.../components/inbox/content/issue-root.tsx | 24 ++-
.../modals/create-modal/issue-description.tsx | 2 +-
.../components/issues/description-input.tsx | 179 ++++++------------
.../issues/issue-detail/main-content.tsx | 23 ++-
.../issues/peek-overview/issue-detail.tsx | 30 +--
.../profile/activity/activity-list.tsx | 2 +-
.../activity/profile-activity-list.tsx | 2 +-
web/core/hooks/use-issue-description.ts | 50 +++++
.../services/inbox/inbox-issue.service.ts | 37 +++-
web/core/services/issue/issue.service.ts | 29 +++
.../services/page/project-page.service.ts | 5 -
47 files changed, 1044 insertions(+), 283 deletions(-)
create mode 100644 live/.prettierignore
create mode 100644 live/.prettierrc
create mode 100644 live/src/core/helpers/document.ts
create mode 100644 live/src/core/helpers/issue.ts
create mode 100644 packages/editor/src/core/components/editors/rich-text/collaborative-editor.tsx
create mode 100644 packages/editor/src/core/components/editors/rich-text/collaborative-read-only-editor.tsx
rename packages/editor/src/core/hooks/{use-collaborative-editor.ts => use-collaborative-document-editor.ts} (93%)
rename packages/editor/src/core/hooks/{use-read-only-collaborative-editor.ts => use-collaborative-document-read-only-editor.ts} (90%)
create mode 100644 packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
create mode 100644 packages/editor/src/core/hooks/use-collaborative-rich-text-read-only-editor.ts
create mode 100644 packages/editor/src/core/providers/custom-collaboration-provider.ts
create mode 100644 packages/editor/src/core/providers/index.ts
rename packages/editor/src/core/types/{collaboration.ts => collaboration-hook.ts} (60%)
create mode 100644 web/core/components/editor/rich-text-editor/collaborative-editor.tsx
create mode 100644 web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx
rename web/core/components/editor/rich-text-editor/{rich-text-editor.tsx => editor.tsx} (100%)
rename web/core/components/editor/rich-text-editor/{rich-text-read-only-editor.tsx => read-only-editor.tsx} (100%)
create mode 100644 web/core/hooks/use-issue-description.ts
diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py
index 7348559ca03..2e358b5cf4f 100644
--- a/apiserver/plane/app/views/inbox/base.py
+++ b/apiserver/plane/app/views/inbox/base.py
@@ -1,6 +1,7 @@
# Python imports
import json
import requests
+import base64
# Django import
from django.utils import timezone
@@ -11,6 +12,7 @@
from django.db.models import Value, UUIDField
from django.db.models.functions import Coalesce
from django.http import StreamingHttpResponse
+from django.conf import settings
# Third party imports
@@ -43,7 +45,6 @@
class InboxViewSet(BaseViewSet):
-
serializer_class = InboxSerializer
model = Inbox
@@ -92,7 +93,6 @@ def destroy(self, request, slug, project_id, pk):
class InboxIssueViewSet(BaseViewSet):
-
serializer_class = InboxIssueSerializer
model = InboxIssue
@@ -684,24 +684,44 @@ 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": issue.description_binary,
+ "original_document": base64_description,
"updates": request.data.get("description_binary"),
}
- scheme = request.scheme
- host = request.get_host()
- base_url = f"{scheme}://{host}/resolve-document-conflicts/"
- response = requests.post(base_url, data=data, headers=None)
+ base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
+ response = requests.post(base_url, json=data, headers=None)
if response.status_code == 200:
- issue.description = request.data.get(
+ issue.description = response.json().get(
"description", issue.description
)
issue.description_html = response.json().get("description_html")
- issue.description_binary = response.json().get(
+ response_description_binary = response.json().get(
"description_binary"
)
+ issue.description_binary = base64.b64decode(
+ response_description_binary
+ )
issue.save()
- return Response(status=status.HTTP_204_NO_CONTENT)
+
+ 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)
diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py
index eb4010c5360..ad6db1ce7df 100644
--- a/apiserver/plane/app/views/issue/base.py
+++ b/apiserver/plane/app/views/issue/base.py
@@ -1,6 +1,7 @@
# Python imports
import json
import requests
+import base64
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
@@ -22,6 +23,7 @@
from django.http import StreamingHttpResponse
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
+from django.conf import settings
# Third Party imports
from rest_framework import status
@@ -718,25 +720,45 @@ 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": issue.description_binary,
+ "original_document": base64_description,
"updates": request.data.get("description_binary"),
}
- scheme = request.scheme
- host = request.get_host()
- base_url = f"{scheme}://{host}/resolve-document-conflicts/"
- response = requests.post(base_url, data=data, headers=None)
+ base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
+ response = requests.post(base_url, json=data, headers=None)
if response.status_code == 200:
- issue.description = request.data.get(
+ issue.description = response.json().get(
"description", issue.description
)
issue.description_html = response.json().get("description_html")
- issue.description_binary = response.json().get(
+ response_description_binary = response.json().get(
"description_binary"
)
+ issue.description_binary = base64.b64decode(
+ response_description_binary
+ )
issue.save()
- return Response(status=status.HTTP_204_NO_CONTENT)
+
+ 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)
diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py
index 4530d40f7e1..679b394e550 100644
--- a/apiserver/plane/app/views/workspace/draft.py
+++ b/apiserver/plane/app/views/workspace/draft.py
@@ -1,6 +1,7 @@
# Python imports
import json
import requests
+import base64
# Django imports
from django.utils import timezone
@@ -19,6 +20,7 @@
from django.db.models.functions import Coalesce
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
+from django.conf import settings
# Third Party imports
from rest_framework import status
@@ -380,24 +382,44 @@ def stream_data():
@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": issue.description_binary,
+ "original_document": base64_description,
"updates": request.data.get("description_binary"),
}
- scheme = request.scheme
- host = request.get_host()
- base_url = f"{scheme}://{host}/resolve-document-conflicts/"
+ base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
response = requests.post(base_url, data=data, headers=None)
if response.status_code == 200:
- issue.description = request.data.get(
+ issue.description = response.json().get(
"description", issue.description
)
issue.description_html = response.json().get("description_html")
- issue.description_binary = response.json().get(
+ response_description_binary = response.json().get(
"description_binary"
)
+ issue.description_binary = base64.b64decode(
+ response_description_binary
+ )
issue.save()
- return Response(status=status.HTTP_204_NO_CONTENT)
+
+ 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)
diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py
index 6e9c98ce1e7..cb5302588ba 100644
--- a/apiserver/plane/settings/common.py
+++ b/apiserver/plane/settings/common.py
@@ -381,6 +381,7 @@
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
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))
diff --git a/live/.prettierignore b/live/.prettierignore
new file mode 100644
index 00000000000..09a5bb19de2
--- /dev/null
+++ b/live/.prettierignore
@@ -0,0 +1,6 @@
+.next
+.vercel
+.tubro
+out/
+dist/
+node_modules/
\ No newline at end of file
diff --git a/live/.prettierrc b/live/.prettierrc
new file mode 100644
index 00000000000..87d988f1b26
--- /dev/null
+++ b/live/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "printWidth": 120,
+ "tabWidth": 2,
+ "trailingComma": "es5"
+}
diff --git a/live/src/core/helpers/document.ts b/live/src/core/helpers/document.ts
new file mode 100644
index 00000000000..330f35879f7
--- /dev/null
+++ b/live/src/core/helpers/document.ts
@@ -0,0 +1,16 @@
+import * as Y from "yjs";
+
+/**
+ * @description apply updates to a document
+ * @param {Uint8Array} document
+ * @param {Uint8Array} updates
+ * @returns {Uint8Array} conflicts resolved document
+ */
+export const applyUpdatesToBinaryData = (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;
+};
diff --git a/live/src/core/helpers/issue.ts b/live/src/core/helpers/issue.ts
new file mode 100644
index 00000000000..9d9194a9e49
--- /dev/null
+++ b/live/src/core/helpers/issue.ts
@@ -0,0 +1,33 @@
+import { getSchema } from "@tiptap/core";
+import { generateHTML } from "@tiptap/html";
+import { yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
+import * as Y from "yjs";
+// plane editor
+import { CoreEditorExtensionsWithoutProps } from "@plane/editor/lib";
+
+const RICH_TEXT_EDITOR_EXTENSIONS = CoreEditorExtensionsWithoutProps;
+const richTextEditorSchema = getSchema(RICH_TEXT_EDITOR_EXTENSIONS);
+
+export const getAllDocumentFormatsFromRichTextEditorBinaryData = (
+ 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, richTextEditorSchema).toJSON();
+ // convert to HTML
+ const contentHTML = generateHTML(contentJSON, RICH_TEXT_EDITOR_EXTENSIONS);
+
+ return {
+ contentBinaryEncoded: base64Data,
+ contentJSON,
+ contentHTML,
+ };
+};
diff --git a/live/src/core/helpers/page.ts b/live/src/core/helpers/page.ts
index 4e79afe6b88..f0db75f142b 100644
--- a/live/src/core/helpers/page.ts
+++ b/live/src/core/helpers/page.ts
@@ -1,17 +1,16 @@
import { getSchema } from "@tiptap/core";
import { generateHTML, generateJSON } from "@tiptap/html";
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
-import * as Y from "yjs"
+import * as Y from "yjs";
// plane editor
import { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps } from "@plane/editor/lib";
-const DOCUMENT_EDITOR_EXTENSIONS = [
- ...CoreEditorExtensionsWithoutProps,
- ...DocumentEditorExtensionsWithoutProps,
-];
+const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
-export const getAllDocumentFormatsFromBinaryData = (description: Uint8Array): {
+export const getAllDocumentFormatsFromDocumentEditorBinaryData = (
+ description: Uint8Array
+): {
contentBinaryEncoded: string;
contentJSON: object;
contentHTML: string;
@@ -22,10 +21,7 @@ export const getAllDocumentFormatsFromBinaryData = (description: Uint8Array): {
Y.applyUpdate(yDoc, description);
// convert to JSON
const type = yDoc.getXmlFragment("default");
- const contentJSON = yXmlFragmentToProseMirrorRootNode(
- type,
- documentEditorSchema
- ).toJSON();
+ const contentJSON = yXmlFragmentToProseMirrorRootNode(type, documentEditorSchema).toJSON();
// convert to HTML
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
@@ -34,26 +30,21 @@ export const getAllDocumentFormatsFromBinaryData = (description: Uint8Array): {
contentJSON,
contentHTML,
};
-}
+};
-export const getBinaryDataFromHTMLString = (descriptionHTML: string): {
- contentBinary: Uint8Array
+export const getBinaryDataFromDocumentEditorHTMLString = (
+ descriptionHTML: string
+): {
+ contentBinary: Uint8Array;
} => {
// convert HTML to JSON
- const contentJSON = generateJSON(
- descriptionHTML ?? "",
- DOCUMENT_EDITOR_EXTENSIONS
- );
+ const contentJSON = generateJSON(descriptionHTML ?? "", DOCUMENT_EDITOR_EXTENSIONS);
// convert JSON to Y.Doc format
- const transformedData = prosemirrorJSONToYDoc(
- documentEditorSchema,
- contentJSON,
- "default"
- );
+ const transformedData = prosemirrorJSONToYDoc(documentEditorSchema, contentJSON, "default");
// convert Y.Doc to Uint8Array format
const encodedData = Y.encodeStateAsUpdate(transformedData);
return {
- contentBinary: encodedData
- }
-}
\ No newline at end of file
+ contentBinary: encodedData,
+ };
+};
diff --git a/live/src/core/lib/page.ts b/live/src/core/lib/page.ts
index c2110a2b8d3..90eb229815d 100644
--- a/live/src/core/lib/page.ts
+++ b/live/src/core/lib/page.ts
@@ -1,7 +1,7 @@
// helpers
import {
- getAllDocumentFormatsFromBinaryData,
- getBinaryDataFromHTMLString,
+ getAllDocumentFormatsFromDocumentEditorBinaryData,
+ getBinaryDataFromDocumentEditorHTMLString,
} from "@/core/helpers/page.js";
// services
import { PageService } from "@/core/services/page.service.js";
@@ -12,12 +12,10 @@ export const updatePageDescription = async (
params: URLSearchParams,
pageId: string,
updatedDescription: Uint8Array,
- cookie: string | undefined,
+ cookie: string | undefined
) => {
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();
@@ -25,7 +23,7 @@ export const updatePageDescription = async (
if (!workspaceSlug || !projectId || !cookie) return;
const { contentBinaryEncoded, contentHTML, contentJSON } =
- getAllDocumentFormatsFromBinaryData(updatedDescription);
+ getAllDocumentFormatsFromDocumentEditorBinaryData(updatedDescription);
try {
const payload = {
description_binary: contentBinaryEncoded,
@@ -33,13 +31,7 @@ export const updatePageDescription = async (
description: contentJSON,
};
- await pageService.updateDescription(
- workspaceSlug,
- projectId,
- pageId,
- payload,
- cookie,
- );
+ await pageService.updateDescription(workspaceSlug, projectId, pageId, payload, cookie);
} catch (error) {
manualLogger.error("Update error:", error);
throw error;
@@ -50,26 +42,16 @@ const fetchDescriptionHTMLAndTransform = async (
workspaceSlug: string,
projectId: string,
pageId: string,
- cookie: string,
+ cookie: string
) => {
if (!workspaceSlug || !projectId || !cookie) return;
try {
- const pageDetails = await pageService.fetchDetails(
- workspaceSlug,
- projectId,
- pageId,
- cookie,
- );
- const { contentBinary } = getBinaryDataFromHTMLString(
- pageDetails.description_html ?? "",
- );
+ const pageDetails = await pageService.fetchDetails(workspaceSlug, projectId, pageId, cookie);
+ const { contentBinary } = getBinaryDataFromDocumentEditorHTMLString(pageDetails.description_html ?? "");
return contentBinary;
} catch (error) {
- manualLogger.error(
- "Error while transforming from HTML to Uint8Array",
- error,
- );
+ manualLogger.error("Error while transforming from HTML to Uint8Array", error);
throw error;
}
};
@@ -77,28 +59,18 @@ const fetchDescriptionHTMLAndTransform = async (
export const fetchPageDescriptionBinary = async (
params: URLSearchParams,
pageId: string,
- cookie: string | undefined,
+ cookie: string | undefined
) => {
const workspaceSlug = params.get("workspaceSlug")?.toString();
const projectId = params.get("projectId")?.toString();
if (!workspaceSlug || !projectId || !cookie) return null;
try {
- const response = await pageService.fetchDescriptionBinary(
- workspaceSlug,
- projectId,
- pageId,
- cookie,
- );
+ const response = await pageService.fetchDescriptionBinary(workspaceSlug, projectId, pageId, cookie);
const binaryData = new Uint8Array(response);
if (binaryData.byteLength === 0) {
- const binary = await fetchDescriptionHTMLAndTransform(
- workspaceSlug,
- projectId,
- pageId,
- cookie,
- );
+ const binary = await fetchDescriptionHTMLAndTransform(workspaceSlug, projectId, pageId, cookie);
if (binary) {
return binary;
}
diff --git a/live/src/server.ts b/live/src/server.ts
index 1868b86c198..b1d2342a59a 100644
--- a/live/src/server.ts
+++ b/live/src/server.ts
@@ -5,16 +5,15 @@ import expressWs from "express-ws";
import * as Sentry from "@sentry/node";
import compression from "compression";
import helmet from "helmet";
-
-// cors
import cors from "cors";
-
+import * as Y from "yjs";
// core hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
-
// helpers
-import { logger, manualLogger } from "@/core/helpers/logger.js";
import { errorHandler } from "@/core/helpers/error-handler.js";
+import { applyUpdatesToBinaryData } from "@/core/helpers/document.js";
+import { getAllDocumentFormatsFromRichTextEditorBinaryData } from "@/core/helpers/issue.js";
+import { logger, manualLogger } from "@/core/helpers/logger.js";
const app = express();
expressWs(app);
@@ -29,7 +28,7 @@ app.use(
compression({
level: 6,
threshold: 5 * 1000,
- }),
+ })
);
// Logging middleware
@@ -62,6 +61,47 @@ router.ws("/collaboration", (ws, req) => {
}
});
+app.post("/resolve-document-conflicts", (req, res) => {
+ const { original_document, updates } = req.body;
+ try {
+ if (original_document === undefined || updates === undefined) {
+ res.status(400).send({
+ message: "Missing required fields",
+ });
+ throw new Error("Missing required fields");
+ }
+ // convert from base64 to buffer
+ const originalDocumentBuffer = original_document ? Buffer.from(original_document, "base64") : null;
+ const updatesBuffer = updates ? Buffer.from(updates, "base64") : 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) {
+ const yDoc = new Y.Doc();
+ Y.applyUpdate(yDoc, decodedUpdates);
+ resolvedDocument = Y.encodeStateAsUpdate(yDoc);
+ } else {
+ resolvedDocument = applyUpdatesToBinaryData(decodedOriginalDocument, decodedUpdates);
+ }
+
+ const { contentBinaryEncoded, contentHTML, contentJSON } =
+ getAllDocumentFormatsFromRichTextEditorBinaryData(resolvedDocument);
+
+ res.status(200).json({
+ description_html: contentHTML,
+ description_binary: contentBinaryEncoded,
+ description: contentJSON,
+ });
+ } catch (error) {
+ console.log("error", error);
+ res.status(500).send({
+ message: "Internal server error",
+ });
+ }
+});
+
app.use(process.env.LIVE_BASE_PATH || "/live", router);
app.use((_req, res) => {
@@ -82,9 +122,7 @@ const gracefulShutdown = async () => {
try {
// Close the HocusPocus server WebSocket connections
await HocusPocusServer.destroy();
- manualLogger.info(
- "HocusPocus server WebSocket connections closed gracefully.",
- );
+ manualLogger.info("HocusPocus server WebSocket connections closed gracefully.");
// Close the Express server
liveServer.close(() => {
diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx
index a008d5c60ba..f8153fbce12 100644
--- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx
+++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx
@@ -8,7 +8,7 @@ import { IssueWidget } from "@/extensions";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
-import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor";
+import { useCollaborativeDocumentEditor } from "@/hooks/use-collaborative-document-editor";
// types
import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types";
@@ -42,7 +42,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
}
// use document editor
- const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
+ const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeDocumentEditor({
disabledExtensions,
editorClassName,
embedHandler,
diff --git a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx
index aa925abece4..90de2e84c62 100644
--- a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx
+++ b/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx
@@ -8,7 +8,7 @@ import { IssueWidget } from "@/extensions";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
-import { useReadOnlyCollaborativeEditor } from "@/hooks/use-read-only-collaborative-editor";
+import { useCollaborativeDocumentReadOnlyEditor } from "@/hooks/use-collaborative-document-read-only-editor";
// types
import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/types";
@@ -36,7 +36,7 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn
);
}
- const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({
+ const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeDocumentReadOnlyEditor({
editorClassName,
extensions,
fileHandler,
diff --git a/packages/editor/src/core/components/editors/editor-wrapper.tsx b/packages/editor/src/core/components/editors/editor-wrapper.tsx
index 3e00dc2afdb..8c41a93a37f 100644
--- a/packages/editor/src/core/components/editors/editor-wrapper.tsx
+++ b/packages/editor/src/core/components/editors/editor-wrapper.tsx
@@ -1,4 +1,4 @@
-import { Editor, Extension } from "@tiptap/core";
+import { AnyExtension, Editor } from "@tiptap/core";
// components
import { EditorContainer } from "@/components/editors";
// constants
@@ -12,7 +12,7 @@ import { EditorContentWrapper } from "./editor-content";
type Props = IEditorProps & {
children?: (editor: Editor) => React.ReactNode;
- extensions: Extension[];
+ extensions: AnyExtension[];
};
export const EditorWrapper: React.FC = (props) => {
diff --git a/packages/editor/src/core/components/editors/rich-text/collaborative-editor.tsx b/packages/editor/src/core/components/editors/rich-text/collaborative-editor.tsx
new file mode 100644
index 00000000000..a96daef3325
--- /dev/null
+++ b/packages/editor/src/core/components/editors/rich-text/collaborative-editor.tsx
@@ -0,0 +1,72 @@
+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 (
+
+
+
+
+
+
+ );
+};
+
+const CollaborativeRichTextEditorWithRef = React.forwardRef(
+ (props, ref) => (
+ } />
+ )
+);
+
+CollaborativeRichTextEditorWithRef.displayName = "CollaborativeRichTextEditorWithRef";
+
+export { CollaborativeRichTextEditorWithRef };
diff --git a/packages/editor/src/core/components/editors/rich-text/collaborative-read-only-editor.tsx b/packages/editor/src/core/components/editors/rich-text/collaborative-read-only-editor.tsx
new file mode 100644
index 00000000000..050d97cae61
--- /dev/null
+++ b/packages/editor/src/core/components/editors/rich-text/collaborative-read-only-editor.tsx
@@ -0,0 +1,70 @@
+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 (
+
+
+
+
+
+
+ );
+};
+
+const CollaborativeRichTextReadOnlyEditorWithRef = React.forwardRef<
+ EditorReadOnlyRefApi,
+ ICollaborativeRichTextReadOnlyEditor
+>((props, ref) => (
+ }
+ />
+));
+
+CollaborativeRichTextReadOnlyEditorWithRef.displayName = "CollaborativeRichTextReadOnlyEditorWithRef";
+
+export { CollaborativeRichTextReadOnlyEditorWithRef };
diff --git a/packages/editor/src/core/components/editors/rich-text/index.ts b/packages/editor/src/core/components/editors/rich-text/index.ts
index b2ba8682a3c..3053a54112d 100644
--- a/packages/editor/src/core/components/editors/rich-text/index.ts
+++ b/packages/editor/src/core/components/editors/rich-text/index.ts
@@ -1,2 +1,4 @@
+export * from "./collaborative-editor";
+export * from "./collaborative-read-only-editor";
export * from "./editor";
export * from "./read-only-editor";
diff --git a/packages/editor/src/core/helpers/yjs.ts b/packages/editor/src/core/helpers/yjs.ts
index ffd9367107d..40a35857199 100644
--- a/packages/editor/src/core/helpers/yjs.ts
+++ b/packages/editor/src/core/helpers/yjs.ts
@@ -1,3 +1,7 @@
+import { CoreEditorExtensionsWithoutProps } from "@/extensions";
+import { getSchema } from "@tiptap/core";
+import { generateJSON } from "@tiptap/html";
+import { prosemirrorJSONToYDoc } from "y-prosemirror";
import * as Y from "yjs";
/**
@@ -14,3 +18,15 @@ export const applyUpdates = (document: Uint8Array, updates: Uint8Array): Uint8Ar
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
return encodedDoc;
};
+
+const richTextEditorSchema = getSchema(CoreEditorExtensionsWithoutProps);
+export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: string): Uint8Array => {
+ // convert HTML to JSON
+ const contentJSON = generateJSON(descriptionHTML ?? "", CoreEditorExtensionsWithoutProps);
+ // 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;
+};
diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-document-editor.ts
similarity index 93%
rename from packages/editor/src/core/hooks/use-collaborative-editor.ts
rename to packages/editor/src/core/hooks/use-collaborative-document-editor.ts
index 5a004bff284..d3701c1b71a 100644
--- a/packages/editor/src/core/hooks/use-collaborative-editor.ts
+++ b/packages/editor/src/core/hooks/use-collaborative-document-editor.ts
@@ -9,9 +9,9 @@ import { useEditor } from "@/hooks/use-editor";
// plane editor extensions
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
// types
-import { TCollaborativeEditorProps } from "@/types";
+import { TCollaborativeDocumentEditorHookProps } from "@/types";
-export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
+export const useCollaborativeDocumentEditor = (props: TCollaborativeDocumentEditorHookProps) => {
const {
disabledExtensions,
editorClassName,
@@ -100,7 +100,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
forwardedRef,
mentionHandler,
placeholder,
- provider,
+ providerDocument: provider.document,
tabIndex,
});
diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-document-read-only-editor.ts
similarity index 90%
rename from packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts
rename to packages/editor/src/core/hooks/use-collaborative-document-read-only-editor.ts
index 9fa73c3ecb1..ffad5f04b94 100644
--- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts
+++ b/packages/editor/src/core/hooks/use-collaborative-document-read-only-editor.ts
@@ -7,9 +7,9 @@ import { HeadingListExtension } from "@/extensions";
// hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
-import { TReadOnlyCollaborativeEditorProps } from "@/types";
+import { TCollaborativeDocumentReadOnlyEditorHookProps } from "@/types";
-export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEditorProps) => {
+export const useCollaborativeDocumentReadOnlyEditor = (props: TCollaborativeDocumentReadOnlyEditorHookProps) => {
const {
editorClassName,
editorProps = {},
@@ -79,7 +79,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
forwardedRef,
handleEditorReady,
mentionHandler,
- provider,
+ providerDocument: provider.document,
});
return {
diff --git a/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
new file mode 100644
index 00000000000..17d0ab1aaf1
--- /dev/null
+++ b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
@@ -0,0 +1,72 @@
+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 (value.length > 0) {
+ Y.applyUpdate(provider.document, value);
+ }
+ }, [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,
+ };
+};
diff --git a/packages/editor/src/core/hooks/use-collaborative-rich-text-read-only-editor.ts b/packages/editor/src/core/hooks/use-collaborative-rich-text-read-only-editor.ts
new file mode 100644
index 00000000000..be5b915fdbc
--- /dev/null
+++ b/packages/editor/src/core/hooks/use-collaborative-rich-text-read-only-editor.ts
@@ -0,0 +1,64 @@
+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,
+ };
+};
diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts
index beee9c929d0..a8967e110d7 100644
--- a/packages/editor/src/core/hooks/use-editor.ts
+++ b/packages/editor/src/core/hooks/use-editor.ts
@@ -1,5 +1,4 @@
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
-import { HocuspocusProvider } from "@hocuspocus/provider";
import { DOMSerializer } from "@tiptap/pm/model";
import { Selection } from "@tiptap/pm/state";
import { EditorProps } from "@tiptap/pm/view";
@@ -34,7 +33,7 @@ export interface CustomEditorProps {
};
onChange?: (json: object, html: string) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
- provider?: HocuspocusProvider;
+ providerDocument?: Y.Doc;
tabIndex?: number;
// undefined when prop is not passed, null if intentionally passed to stop
// swr syncing
@@ -55,7 +54,7 @@ export const useEditor = (props: CustomEditorProps) => {
mentionHandler,
onChange,
placeholder,
- provider,
+ providerDocument,
tabIndex,
value,
} = props;
@@ -195,7 +194,7 @@ export const useEditor = (props: CustomEditorProps) => {
return markdownOutput;
},
getDocument: () => {
- const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
+ const documentBinary = providerDocument ? Y.encodeStateAsUpdate(providerDocument) : null;
const documentHTML = editorRef.current?.getHTML() ?? "";
const documentJSON = editorRef.current?.getJSON() ?? null;
@@ -273,7 +272,7 @@ export const useEditor = (props: CustomEditorProps) => {
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
}),
setProviderDocument: (value) => {
- const document = provider?.document;
+ const document = providerDocument;
if (!document) return;
Y.applyUpdate(document, value);
},
diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts
index 23ce023adcd..cde6a8937a9 100644
--- a/packages/editor/src/core/hooks/use-read-only-editor.ts
+++ b/packages/editor/src/core/hooks/use-read-only-editor.ts
@@ -1,5 +1,4 @@
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
-import { HocuspocusProvider } from "@hocuspocus/provider";
import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
import * as Y from "yjs";
@@ -24,7 +23,7 @@ interface CustomReadOnlyEditorProps {
mentionHandler: {
highlights: () => Promise;
};
- provider?: HocuspocusProvider;
+ providerDocument?: Y.Doc;
}
export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
@@ -37,7 +36,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
fileHandler,
handleEditorReady,
mentionHandler,
- provider,
+ providerDocument,
} = props;
const editor = useCustomEditor({
@@ -86,7 +85,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
return markdownOutput;
},
getDocument: () => {
- const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
+ const documentBinary = providerDocument ? Y.encodeStateAsUpdate(providerDocument) : null;
const documentHTML = editorRef.current?.getHTML() ?? "";
const documentJSON = editorRef.current?.getJSON() ?? null;
diff --git a/packages/editor/src/core/providers/custom-collaboration-provider.ts b/packages/editor/src/core/providers/custom-collaboration-provider.ts
new file mode 100644
index 00000000000..3375b58c5c2
--- /dev/null
+++ b/packages/editor/src/core/providers/custom-collaboration-provider.ts
@@ -0,0 +1,57 @@
+import * as Y from "yjs";
+
+export interface CompleteCollaboratorProviderConfiguration {
+ /**
+ * 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> &
+ Partial;
+
+export class CustomCollaborationProvider {
+ public configuration: CompleteCollaboratorProviderConfiguration = {
+ name: "",
+ document: new Y.Doc(),
+ onChange: () => {},
+ };
+
+ constructor(configuration: CollaborationProviderConfiguration) {
+ this.setConfiguration(configuration);
+ this.document.on("update", this.documentUpdateHandler.bind(this));
+ this.document.on("destroy", this.documentDestroyHandler.bind(this));
+ }
+
+ public setConfiguration(configuration: Partial = {}): void {
+ this.configuration = {
+ ...this.configuration,
+ ...configuration,
+ };
+ }
+
+ get document() {
+ return this.configuration.document;
+ }
+
+ async documentUpdateHandler(_update: Uint8Array, origin: any) {
+ // 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);
+ }
+}
diff --git a/packages/editor/src/core/providers/index.ts b/packages/editor/src/core/providers/index.ts
new file mode 100644
index 00000000000..36e7996394a
--- /dev/null
+++ b/packages/editor/src/core/providers/index.ts
@@ -0,0 +1 @@
+export * from "./custom-collaboration-provider";
diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration-hook.ts
similarity index 60%
rename from packages/editor/src/core/types/collaboration.ts
rename to packages/editor/src/core/types/collaboration-hook.ts
index 60721a5a662..08a6e110b92 100644
--- a/packages/editor/src/core/types/collaboration.ts
+++ b/packages/editor/src/core/types/collaboration-hook.ts
@@ -19,7 +19,7 @@ export type TServerHandler = {
onServerError?: () => void;
};
-type TCollaborativeEditorHookProps = {
+type TCollaborativeEditorHookCommonProps = {
disabledExtensions?: TExtensions[];
editorClassName: string;
editorProps?: EditorProps;
@@ -30,20 +30,38 @@ type TCollaborativeEditorHookProps = {
highlights: () => Promise;
suggestions?: () => Promise;
};
- realtimeConfig: TRealtimeConfig;
- serverHandler?: TServerHandler;
- user: TUserDetails;
};
-export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
- embedHandler?: TEmbedConfig;
+type TCollaborativeEditorHookProps = TCollaborativeEditorHookCommonProps & {
fileHandler: TFileHandler;
forwardedRef?: React.MutableRefObject;
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
};
-export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
+type TCollaborativeReadOnlyEditorHookProps = TCollaborativeEditorHookCommonProps & {
fileHandler: Pick;
forwardedRef?: React.MutableRefObject;
};
+
+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;
+};
diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts
index 31b315c1ca2..e6de198b814 100644
--- a/packages/editor/src/core/types/editor.ts
+++ b/packages/editor/src/core/types/editor.ts
@@ -91,6 +91,12 @@ export interface IRichTextEditor extends IEditorProps {
dragDropEnabled?: boolean;
}
+export interface ICollaborativeRichTextEditor extends Omit {
+ dragDropEnabled?: boolean;
+ onChange: (updatedDescription: Uint8Array) => void;
+ value: Uint8Array;
+}
+
export interface ICollaborativeDocumentEditor
extends Omit {
aiHandler?: TAIHandler;
@@ -121,6 +127,10 @@ export type ILiteTextReadOnlyEditor = IReadOnlyEditorProps;
export type IRichTextReadOnlyEditor = IReadOnlyEditorProps;
+export type ICollaborativeRichTextReadOnlyEditor = Omit & {
+ value: Uint8Array;
+};
+
export interface ICollaborativeDocumentReadOnlyEditor extends Omit {
embedHandler: TEmbedConfig;
handleEditorReady?: (value: boolean) => void;
diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts
index 8da9ed276e5..b4c4ad3625a 100644
--- a/packages/editor/src/core/types/index.ts
+++ b/packages/editor/src/core/types/index.ts
@@ -1,5 +1,5 @@
export * from "./ai";
-export * from "./collaboration";
+export * from "./collaboration-hook";
export * from "./config";
export * from "./editor";
export * from "./embed";
diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts
index 292dc53fb2c..adb8c4c153d 100644
--- a/packages/editor/src/index.ts
+++ b/packages/editor/src/index.ts
@@ -10,6 +10,8 @@ import "src/styles/drag-drop.css";
export {
CollaborativeDocumentEditorWithRef,
CollaborativeDocumentReadOnlyEditorWithRef,
+ CollaborativeRichTextEditorWithRef,
+ CollaborativeRichTextReadOnlyEditorWithRef,
DocumentReadOnlyEditorWithRef,
LiteTextEditorWithRef,
LiteTextReadOnlyEditorWithRef,
diff --git a/web/core/components/core/modals/gpt-assistant-popover.tsx b/web/core/components/core/modals/gpt-assistant-popover.tsx
index 0056977ed6b..99ae0e5863a 100644
--- a/web/core/components/core/modals/gpt-assistant-popover.tsx
+++ b/web/core/components/core/modals/gpt-assistant-popover.tsx
@@ -9,7 +9,7 @@ import { Popover, Transition } from "@headlessui/react";
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// components
-import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
+import { RichTextReadOnlyEditor } from "@/components/editor";
// services
import { AIService } from "@/services/ai.service";
diff --git a/web/core/components/editor/rich-text-editor/collaborative-editor.tsx b/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
new file mode 100644
index 00000000000..9fe4884f5b2
--- /dev/null
+++ b/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
@@ -0,0 +1,107 @@
+import React, { forwardRef, useCallback } from "react";
+import debounce from "lodash/debounce";
+// editor
+import { CollaborativeRichTextEditorWithRef, EditorRefApi, ICollaborativeRichTextEditor } from "@plane/editor";
+// types
+import { IUserLite } from "@plane/types";
+// plane ui
+import { Loader } from "@plane/ui";
+// helpers
+import { cn } from "@/helpers/common.helper";
+import { getEditorFileHandlers } from "@/helpers/editor.helper";
+// hooks
+import { useMember, useMention, useUser } from "@/hooks/store";
+import { useIssueDescription } from "@/hooks/use-issue-description";
+// plane web hooks
+import { useFileSize } from "@/plane-web/hooks/use-file-size";
+
+interface Props extends Omit {
+ descriptionHTML: string;
+ fetchDescription: () => Promise;
+ projectId: string;
+ updateDescription: (data: string) => Promise;
+ uploadFile: (file: File) => Promise;
+ workspaceId: string;
+ workspaceSlug: string;
+}
+
+export const CollaborativeRichTextEditor = forwardRef((props, ref) => {
+ const {
+ containerClassName,
+ descriptionHTML,
+ fetchDescription,
+ workspaceSlug,
+ workspaceId,
+ projectId,
+ updateDescription,
+ 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);
+
+ function isMutableRefObject(ref: React.ForwardedRef): ref is React.MutableRefObject {
+ return !!ref && typeof ref === "object" && "current" in ref;
+ }
+ // use issue description
+ const { descriptionBinary, resolveConflictsAndUpdateDescription } = useIssueDescription({
+ descriptionHTML,
+ fetchDescription,
+ updateDescription,
+ });
+ // use-mention
+ const { mentionHighlights, mentionSuggestions } = useMention({
+ workspaceSlug,
+ projectId,
+ members: projectMemberDetails,
+ user: currentUser,
+ });
+ // file size
+ const { maxFileSize } = useFileSize();
+
+ const debouncedDescriptionSave = useCallback(
+ debounce(async (updatedDescription: Uint8Array) => {
+ const editorRef = isMutableRefObject(ref) ? ref?.current : null;
+ const encodedDescription = Buffer.from(updatedDescription).toString("base64");
+ await resolveConflictsAndUpdateDescription(encodedDescription, editorRef);
+ }, 1500),
+ []
+ );
+
+ if (!descriptionBinary)
+ return (
+
+
+
+ );
+
+ return (
+
+ );
+});
+
+CollaborativeRichTextEditor.displayName = "CollaborativeRichTextEditor";
diff --git a/web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx b/web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx
new file mode 100644
index 00000000000..253acc75e80
--- /dev/null
+++ b/web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx
@@ -0,0 +1,63 @@
+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"
+> & {
+ descriptionHTML: string;
+ fetchDescription: () => Promise;
+ projectId?: string;
+ workspaceSlug: string;
+};
+
+export const CollaborativeRichTextReadOnlyEditor = React.forwardRef<
+ EditorReadOnlyRefApi,
+ RichTextReadOnlyEditorWrapperProps
+>(({ descriptionHTML, fetchDescription, projectId, workspaceSlug, ...props }, ref) => {
+ const { mentionHighlights } = useMention({});
+
+ const { descriptionBinary } = useIssueDescription({
+ descriptionHTML,
+ fetchDescription,
+ });
+
+ if (!descriptionBinary)
+ return (
+
+
+
+ );
+
+ return (
+
+ );
+});
+
+CollaborativeRichTextReadOnlyEditor.displayName = "CollaborativeRichTextReadOnlyEditor";
diff --git a/web/core/components/editor/rich-text-editor/rich-text-editor.tsx b/web/core/components/editor/rich-text-editor/editor.tsx
similarity index 100%
rename from web/core/components/editor/rich-text-editor/rich-text-editor.tsx
rename to web/core/components/editor/rich-text-editor/editor.tsx
diff --git a/web/core/components/editor/rich-text-editor/index.ts b/web/core/components/editor/rich-text-editor/index.ts
index f185d0054e8..3053a54112d 100644
--- a/web/core/components/editor/rich-text-editor/index.ts
+++ b/web/core/components/editor/rich-text-editor/index.ts
@@ -1,2 +1,4 @@
-export * from "./rich-text-editor";
-export * from "./rich-text-read-only-editor";
+export * from "./collaborative-editor";
+export * from "./collaborative-read-only-editor";
+export * from "./editor";
+export * from "./read-only-editor";
diff --git a/web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx b/web/core/components/editor/rich-text-editor/read-only-editor.tsx
similarity index 100%
rename from web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx
rename to web/core/components/editor/rich-text-editor/read-only-editor.tsx
diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx
index 87c8ae6d2c3..044270bfc48 100644
--- a/web/core/components/inbox/content/issue-root.tsx
+++ b/web/core/components/inbox/content/issue-root.tsx
@@ -20,6 +20,9 @@ import {
// hooks
import { useEventTracker, useProjectInbox, useUser } from "@/hooks/store";
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
+// services
+import { InboxIssueService } from "@/services/inbox";
+const inboxIssueService = new InboxIssueService();
// store types
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
@@ -121,15 +124,24 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
) : (
"}
- initialValue={issue.description_html ?? ""}
+ containerClassName="-ml-3 border-none"
+ descriptionHTML={issue.description_html ?? ""}
disabled={!isEditable}
+ fetchDescription={async () => {
+ if (!workspaceSlug || !projectId || !issue.id) return;
+ return await inboxIssueService.fetchDescriptionBinary(workspaceSlug, projectId, issue.id);
+ }}
+ updateDescription={async (data) => {
+ if (!workspaceSlug || !projectId || !issue.id) return;
+ return await inboxIssueService.updateDescriptionBinary(workspaceSlug, projectId, issue.id, {
+ description_binary: data,
+ });
+ }}
+ issueId={issue.id}
issueOperations={issueOperations}
+ projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
- containerClassName="-ml-3 border-none"
+ workspaceSlug={workspaceSlug}
/>
)}
diff --git a/web/core/components/inbox/modals/create-modal/issue-description.tsx b/web/core/components/inbox/modals/create-modal/issue-description.tsx
index b9bad6c11ac..4cf7b3f932c 100644
--- a/web/core/components/inbox/modals/create-modal/issue-description.tsx
+++ b/web/core/components/inbox/modals/create-modal/issue-description.tsx
@@ -10,7 +10,7 @@ import { EFileAssetType } from "@plane/types/src/enums";
// ui
import { Loader } from "@plane/ui";
// components
-import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor";
+import { RichTextEditor } from "@/components/editor";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx
index 8c18618c506..ee3d0ff2a41 100644
--- a/web/core/components/issues/description-input.tsx
+++ b/web/core/components/issues/description-input.tsx
@@ -1,16 +1,13 @@
"use client";
-import { FC, useCallback, useEffect, useState } from "react";
-import debounce from "lodash/debounce";
+import { FC, useRef } from "react";
import { observer } from "mobx-react";
-import { Controller, useForm } from "react-hook-form";
+// plane editor
+import { EditorRefApi } from "@plane/editor";
// types
-import { TIssue } from "@plane/types";
import { EFileAssetType } from "@plane/types/src/enums";
-// ui
-import { Loader } from "@plane/ui";
// components
-import { RichTextEditor, RichTextReadOnlyEditor } from "@/components/editor";
+import { CollaborativeRichTextEditor, CollaborativeRichTextReadOnlyEditor } from "@/components/editor";
import { TIssueOperations } from "@/components/issues/issue-detail";
// helpers
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
@@ -22,136 +19,80 @@ const fileService = new FileService();
export type IssueDescriptionInputProps = {
containerClassName?: string;
- workspaceSlug: string;
- projectId: string;
- issueId: string;
- initialValue: string | undefined;
+ descriptionHTML: string;
disabled?: boolean;
+ fetchDescription: () => Promise;
+ issueId: string;
issueOperations: TIssueOperations;
placeholder?: string | ((isFocused: boolean, value: string) => string);
+ projectId: string;
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
- swrIssueDescription?: string | null | undefined;
+ updateDescription: (data: string) => Promise;
+ workspaceSlug: string;
};
export const IssueDescriptionInput: FC = observer((props) => {
const {
containerClassName,
- workspaceSlug,
- projectId,
- issueId,
+ descriptionHTML,
disabled,
- swrIssueDescription,
- initialValue,
- issueOperations,
- setIsSubmitting,
+ fetchDescription,
+ issueId,
placeholder,
+ projectId,
+ setIsSubmitting,
+ updateDescription,
+ workspaceSlug,
} = props;
-
- const { handleSubmit, reset, control } = useForm({
- defaultValues: {
- description_html: initialValue,
- },
- });
-
- const [localIssueDescription, setLocalIssueDescription] = useState({
- id: issueId,
- description_html: initialValue,
- });
-
- const handleDescriptionFormSubmit = useCallback(
- async (formData: Partial) => {
- await issueOperations.update(workspaceSlug, projectId, issueId, {
- description_html: formData.description_html ?? "",
- });
- },
- [workspaceSlug, projectId, issueId, issueOperations]
- );
-
+ // refs
+ const editorRef = useRef(null);
+ // store hooks
const { getWorkspaceBySlug } = useWorkspace();
- // computed values
- const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string;
-
- // reset form values
- useEffect(() => {
- if (!issueId) return;
- reset({
- id: issueId,
- description_html: initialValue === "" ? "" : initialValue,
- });
- setLocalIssueDescription({
- id: issueId,
- description_html: initialValue === "" ? "" : initialValue,
- });
- }, [initialValue, issueId, reset]);
-
- // ADDING handleDescriptionFormSubmit TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS
- // TODO: Verify the exhaustive-deps warning
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const debouncedFormSave = useCallback(
- debounce(async () => {
- handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
- }, 1500),
- [handleSubmit, issueId]
- );
+ // derived values
+ const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString() ?? "";
return (
<>
- {localIssueDescription.description_html ? (
-
- !disabled ? (
- "}
- value={swrIssueDescription ?? null}
- workspaceSlug={workspaceSlug}
- workspaceId={workspaceId}
- projectId={projectId}
- dragDropEnabled
- onChange={(_description: object, description_html: string) => {
- setIsSubmitting("submitting");
- onChange(description_html);
- debouncedFormSave();
- }}
- placeholder={
- placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)
- }
- containerClassName={containerClassName}
- uploadFile={async (file) => {
- try {
- const { asset_id } = await fileService.uploadProjectAsset(
- workspaceSlug,
- projectId,
- {
- entity_identifier: issueId,
- entity_type: EFileAssetType.ISSUE_DESCRIPTION,
- },
- file
- );
- return asset_id;
- } catch (error) {
- console.log("Error in uploading issue asset:", error);
- throw new Error("Asset upload failed. Please try again later.");
- }
- }}
- />
- ) : (
-
- )
- }
+ {!disabled ? (
+ getDescriptionPlaceholder(isFocused, value)}
+ projectId={projectId}
+ ref={editorRef}
+ updateDescription={updateDescription}
+ uploadFile={async (file) => {
+ try {
+ const { asset_id } = await fileService.uploadProjectAsset(
+ workspaceSlug,
+ projectId,
+ {
+ entity_identifier: issueId,
+ entity_type: EFileAssetType.ISSUE_DESCRIPTION,
+ },
+ file
+ );
+ return asset_id;
+ } catch (error) {
+ console.log("Error in uploading issue asset:", error);
+ throw new Error("Asset upload failed. Please try again later.");
+ }
+ }}
+ workspaceId={workspaceId}
+ workspaceSlug={workspaceSlug}
/>
) : (
-
-
-
+
)}
>
);
diff --git a/web/core/components/issues/issue-detail/main-content.tsx b/web/core/components/issues/issue-detail/main-content.tsx
index 70ac2d250e1..0a6bbd82191 100644
--- a/web/core/components/issues/issue-detail/main-content.tsx
+++ b/web/core/components/issues/issue-detail/main-content.tsx
@@ -19,6 +19,9 @@ import useReloadConfirmations from "@/hooks/use-reload-confirmation";
import useSize from "@/hooks/use-window-size";
// plane web components
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
+// services
+import { IssueService } from "@/services/issue";
+const issueService = new IssueService();
// types
import { TIssueOperations } from "./root";
@@ -87,14 +90,24 @@ export const IssueMainContent: React.FC = observer((props) => {
/>
"}
disabled={!isEditable}
+ fetchDescription={async () => {
+ if (!workspaceSlug || !projectId || !issueId) return;
+ return await issueService.fetchDescriptionBinary(workspaceSlug, projectId, issueId);
+ }}
+ updateDescription={async (data) => {
+ if (!workspaceSlug || !issue.project_id || !issue.id) return;
+ return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
+ description_binary: data,
+ });
+ }}
+ issueId={issue.id}
issueOperations={issueOperations}
+ projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
- containerClassName="-ml-3 border-none"
+ workspaceSlug={workspaceSlug}
/>
{currentUser && (
diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx
index 242ebfd0cee..19bdd2e975b 100644
--- a/web/core/components/issues/peek-overview/issue-detail.tsx
+++ b/web/core/components/issues/peek-overview/issue-detail.tsx
@@ -8,6 +8,9 @@ import { useIssueDetail, useUser } from "@/hooks/store";
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
// plane web components
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
+// services
+import { IssueService } from "@/services/issue";
+const issueService = new IssueService();
// local components
import { IssueDescriptionInput } from "../description-input";
import { IssueReaction } from "../issue-detail/reactions";
@@ -48,13 +51,6 @@ export const PeekOverviewIssueDetails: FC = observer(
const issue = issueId ? getIssueById(issueId) : undefined;
if (!issue || !issue.project_id) return <>>;
- const issueDescription =
- issue.description_html !== undefined || issue.description_html !== null
- ? issue.description_html != ""
- ? issue.description_html
- : ""
- : undefined;
-
return (
{issue.parent_id && (
@@ -80,14 +76,24 @@ export const PeekOverviewIssueDetails: FC
= observer(
/>
"}
disabled={disabled}
+ fetchDescription={async () => {
+ if (!workspaceSlug || !issue.project_id || !issue.id) return;
+ return await issueService.fetchDescriptionBinary(workspaceSlug, issue.project_id, issue.id);
+ }}
+ updateDescription={async (data) => {
+ if (!workspaceSlug || !issue.project_id || !issue.id) return;
+ return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
+ description_binary: data,
+ });
+ }}
+ issueId={issue.id}
issueOperations={issueOperations}
+ projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
- containerClassName="-ml-3 border-none"
+ workspaceSlug={workspaceSlug}
/>
{currentUser && (
diff --git a/web/core/components/profile/activity/activity-list.tsx b/web/core/components/profile/activity/activity-list.tsx
index bdb6c6f9356..6b83a92bbde 100644
--- a/web/core/components/profile/activity/activity-list.tsx
+++ b/web/core/components/profile/activity/activity-list.tsx
@@ -8,7 +8,7 @@ import { IUserActivityResponse } from "@plane/types";
// components
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
// editor
-import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
+import { RichTextReadOnlyEditor } from "@/components/editor";
// ui
import { ActivitySettingsLoader } from "@/components/ui";
// helpers
diff --git a/web/core/components/profile/activity/profile-activity-list.tsx b/web/core/components/profile/activity/profile-activity-list.tsx
index 6878fe9b3c7..0fe9b44f9a8 100644
--- a/web/core/components/profile/activity/profile-activity-list.tsx
+++ b/web/core/components/profile/activity/profile-activity-list.tsx
@@ -7,7 +7,7 @@ import useSWR from "swr";
import { History, MessageSquare } from "lucide-react";
// hooks
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
-import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
+import { RichTextReadOnlyEditor } from "@/components/editor";
import { ActivitySettingsLoader } from "@/components/ui";
// constants
import { USER_ACTIVITY } from "@/constants/fetch-keys";
diff --git a/web/core/hooks/use-issue-description.ts b/web/core/hooks/use-issue-description.ts
new file mode 100644
index 00000000000..d2ae4920021
--- /dev/null
+++ b/web/core/hooks/use-issue-description.ts
@@ -0,0 +1,50 @@
+import { useCallback, useEffect, useState } from "react";
+// plane editor
+import { EditorRefApi, getBinaryDataFromRichTextEditorHTMLString } from "@plane/editor";
+
+type TArgs = {
+ descriptionHTML: string | null;
+ fetchDescription: () => Promise;
+ updateDescription?: (data: string) => Promise;
+};
+
+export const useIssueDescription = (args: TArgs) => {
+ const { descriptionHTML, fetchDescription, updateDescription } = args;
+ // states
+ const [descriptionBinary, setDescriptionBinary] = useState(null);
+ // update description
+ const resolveConflictsAndUpdateDescription = useCallback(
+ async (encodedDescription: string, editorRef: EditorRefApi | null) => {
+ if (!updateDescription) return;
+ const conflictFreeEncodedDescription = await updateDescription(encodedDescription);
+ const decodedDescription = conflictFreeEncodedDescription
+ ? new Uint8Array(conflictFreeEncodedDescription)
+ : new Uint8Array();
+ editorRef?.setProviderDocument(decodedDescription);
+ },
+ [updateDescription]
+ );
+
+ useEffect(() => {
+ if (descriptionBinary) return;
+ // fetch latest binary description
+ const fetchDecodedDescription = async () => {
+ const encodedDescription = await fetchDescription();
+ let decodedDescription = encodedDescription ? new Uint8Array(encodedDescription) : new Uint8Array();
+ // if there's no binary data present, convert existing HTML string to binary
+ if (decodedDescription.length === 0) {
+ decodedDescription = getBinaryDataFromRichTextEditorHTMLString(descriptionHTML ?? "");
+ } else {
+ // decode binary string
+ decodedDescription = new Uint8Array(encodedDescription);
+ }
+ setDescriptionBinary(decodedDescription);
+ };
+ fetchDecodedDescription();
+ }, [descriptionBinary, descriptionHTML, fetchDescription]);
+
+ return {
+ descriptionBinary,
+ resolveConflictsAndUpdateDescription,
+ };
+};
diff --git a/web/core/services/inbox/inbox-issue.service.ts b/web/core/services/inbox/inbox-issue.service.ts
index 61aaeb84906..3ac13bce6d2 100644
--- a/web/core/services/inbox/inbox-issue.service.ts
+++ b/web/core/services/inbox/inbox-issue.service.ts
@@ -1,5 +1,5 @@
// types
-import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm } from "@plane/types";
+import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm, TDocumentPayload } from "@plane/types";
import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service";
// helpers
@@ -76,6 +76,41 @@ export class InboxIssueService extends APIService {
});
}
+ async fetchDescriptionBinary(workspaceSlug: string, projectId: string, inboxIssueId: string): Promise {
+ return this.get(
+ `/api/workspaces/${workspaceSlug}/projects/${projectId}/inbox-issues/${inboxIssueId}/description/`,
+ {
+ headers: {
+ "Content-Type": "application/octet-stream",
+ },
+ responseType: "arraybuffer",
+ }
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async updateDescriptionBinary(
+ workspaceSlug: string,
+ projectId: string,
+ inboxIssueId: string,
+ data: Pick
+ ): Promise {
+ 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 {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake/`)
.then((response) => response?.data)
diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts
index 76394a4d1f7..e9bcf351d7e 100644
--- a/web/core/services/issue/issue.service.ts
+++ b/web/core/services/issue/issue.service.ts
@@ -3,6 +3,7 @@ import * as Sentry from "@sentry/nextjs";
import type {
IIssueDisplayProperties,
TBulkOperationsPayload,
+ TDocumentPayload,
TIssue,
TIssueActivity,
TIssueLink,
@@ -371,4 +372,32 @@ export class IssueService extends APIService {
throw error?.response?.data;
});
}
+
+ async fetchDescriptionBinary(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/description/`, {
+ headers: {
+ "Content-Type": "application/octet-stream",
+ },
+ responseType: "arraybuffer",
+ })
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async updateDescriptionBinary(
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string,
+ data: Pick
+ ): Promise {
+ return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/description/`, data, {
+ responseType: "arraybuffer",
+ })
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
}
diff --git a/web/core/services/page/project-page.service.ts b/web/core/services/page/project-page.service.ts
index 00d9401a69a..e2f22d5ad3c 100644
--- a/web/core/services/page/project-page.service.ts
+++ b/web/core/services/page/project-page.service.ts
@@ -4,15 +4,10 @@ import { TDocumentPayload, TPage } from "@plane/types";
import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
-import { FileUploadService } from "@/services/file-upload.service";
export class ProjectPageService extends APIService {
- private fileUploadService: FileUploadService;
-
constructor() {
super(API_BASE_URL);
- // upload service
- this.fileUploadService = new FileUploadService();
}
async fetchAll(workspaceSlug: string, projectId: string): Promise {
From d3b443ee923c03abb7b578e9f358a126c9671409 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Fri, 25 Oct 2024 15:20:51 +0530
Subject: [PATCH 03/21] chore: fix submitting status
---
.../rich-text-editor/collaborative-editor.tsx | 51 +----------------
.../components/issues/description-input.tsx | 57 +++++++++++++++++--
web/core/hooks/use-issue-description.ts | 7 ++-
web/core/hooks/use-page-fallback.ts | 4 +-
4 files changed, 63 insertions(+), 56 deletions(-)
diff --git a/web/core/components/editor/rich-text-editor/collaborative-editor.tsx b/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
index 9fe4884f5b2..6f4dd15e2e5 100644
--- a/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
+++ b/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
@@ -1,42 +1,25 @@
-import React, { forwardRef, useCallback } from "react";
-import debounce from "lodash/debounce";
+import React, { forwardRef } from "react";
// editor
import { CollaborativeRichTextEditorWithRef, EditorRefApi, ICollaborativeRichTextEditor } from "@plane/editor";
// types
import { IUserLite } from "@plane/types";
-// plane ui
-import { Loader } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { useMember, useMention, useUser } from "@/hooks/store";
-import { useIssueDescription } from "@/hooks/use-issue-description";
// plane web hooks
import { useFileSize } from "@/plane-web/hooks/use-file-size";
-interface Props extends Omit {
- descriptionHTML: string;
- fetchDescription: () => Promise;
+interface Props extends Omit {
projectId: string;
- updateDescription: (data: string) => Promise;
uploadFile: (file: File) => Promise;
workspaceId: string;
workspaceSlug: string;
}
export const CollaborativeRichTextEditor = forwardRef((props, ref) => {
- const {
- containerClassName,
- descriptionHTML,
- fetchDescription,
- workspaceSlug,
- workspaceId,
- projectId,
- updateDescription,
- uploadFile,
- ...rest
- } = props;
+ const { containerClassName, workspaceSlug, workspaceId, projectId, uploadFile, ...rest } = props;
// store hooks
const { data: currentUser } = useUser();
const {
@@ -46,16 +29,6 @@ export const CollaborativeRichTextEditor = forwardRef((prop
// derived values
const projectMemberIds = getProjectMemberIds(projectId);
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
-
- function isMutableRefObject(ref: React.ForwardedRef): ref is React.MutableRefObject {
- return !!ref && typeof ref === "object" && "current" in ref;
- }
- // use issue description
- const { descriptionBinary, resolveConflictsAndUpdateDescription } = useIssueDescription({
- descriptionHTML,
- fetchDescription,
- updateDescription,
- });
// use-mention
const { mentionHighlights, mentionSuggestions } = useMention({
workspaceSlug,
@@ -66,22 +39,6 @@ export const CollaborativeRichTextEditor = forwardRef((prop
// file size
const { maxFileSize } = useFileSize();
- const debouncedDescriptionSave = useCallback(
- debounce(async (updatedDescription: Uint8Array) => {
- const editorRef = isMutableRefObject(ref) ? ref?.current : null;
- const encodedDescription = Buffer.from(updatedDescription).toString("base64");
- await resolveConflictsAndUpdateDescription(encodedDescription, editorRef);
- }, 1500),
- []
- );
-
- if (!descriptionBinary)
- return (
-
-
-
- );
-
return (
((prop
highlights: mentionHighlights,
suggestions: mentionSuggestions,
}}
- onChange={debouncedDescriptionSave}
- value={descriptionBinary}
{...rest}
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
/>
diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx
index ee3d0ff2a41..32faa0b6cc8 100644
--- a/web/core/components/issues/description-input.tsx
+++ b/web/core/components/issues/description-input.tsx
@@ -1,11 +1,14 @@
"use client";
-import { FC, useRef } from "react";
+import { FC, useCallback, useRef } from "react";
+import debounce from "lodash/debounce";
import { observer } from "mobx-react";
// plane editor
-import { EditorRefApi } from "@plane/editor";
+import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
// types
import { EFileAssetType } from "@plane/types/src/enums";
+// plane ui
+import { Loader } from "@plane/ui";
// components
import { CollaborativeRichTextEditor, CollaborativeRichTextReadOnlyEditor } from "@/components/editor";
import { TIssueOperations } from "@/components/issues/issue-detail";
@@ -13,6 +16,7 @@ import { TIssueOperations } from "@/components/issues/issue-detail";
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
// hooks
import { useWorkspace } from "@/hooks/store";
+import { useIssueDescription } from "@/hooks/use-issue-description";
// services
import { FileService } from "@/services/file.service";
const fileService = new FileService();
@@ -50,20 +54,63 @@ export const IssueDescriptionInput: FC = observer((p
const { getWorkspaceBySlug } = useWorkspace();
// derived values
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString() ?? "";
+ // use issue description
+ const { descriptionBinary, resolveConflictsAndUpdateDescription } = useIssueDescription({
+ descriptionHTML,
+ id: issueId,
+ fetchDescription,
+ updateDescription,
+ });
+
+ const debouncedDescriptionSave = useCallback(
+ debounce(async (updatedDescription: Uint8Array) => {
+ const editor = editorRef.current;
+ if (!editor) return;
+ const encodedDescription = convertBinaryDataToBase64String(updatedDescription);
+ await resolveConflictsAndUpdateDescription(encodedDescription, editor);
+ setIsSubmitting("submitted");
+ }, 1500),
+ []
+ );
+
+ if (!descriptionBinary)
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
return (
<>
{!disabled ? (
{
+ setIsSubmitting("submitting");
+ debouncedDescriptionSave(val);
+ }}
dragDropEnabled
- fetchDescription={fetchDescription}
id={issueId}
placeholder={placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)}
projectId={projectId}
ref={editorRef}
- updateDescription={updateDescription}
uploadFile={async (file) => {
try {
const { asset_id } = await fileService.uploadProjectAsset(
diff --git a/web/core/hooks/use-issue-description.ts b/web/core/hooks/use-issue-description.ts
index d2ae4920021..ba2c5a9d360 100644
--- a/web/core/hooks/use-issue-description.ts
+++ b/web/core/hooks/use-issue-description.ts
@@ -5,11 +5,12 @@ import { EditorRefApi, getBinaryDataFromRichTextEditorHTMLString } from "@plane/
type TArgs = {
descriptionHTML: string | null;
fetchDescription: () => Promise;
+ id: string;
updateDescription?: (data: string) => Promise;
};
export const useIssueDescription = (args: TArgs) => {
- const { descriptionHTML, fetchDescription, updateDescription } = args;
+ const { descriptionHTML, fetchDescription, id, updateDescription } = args;
// states
const [descriptionBinary, setDescriptionBinary] = useState(null);
// update description
@@ -43,6 +44,10 @@ export const useIssueDescription = (args: TArgs) => {
fetchDecodedDescription();
}, [descriptionBinary, descriptionHTML, fetchDescription]);
+ useEffect(() => {
+ setDescriptionBinary(null);
+ }, [id]);
+
return {
descriptionBinary,
resolveConflictsAndUpdateDescription,
diff --git a/web/core/hooks/use-page-fallback.ts b/web/core/hooks/use-page-fallback.ts
index 9f5ef348293..d07aac0d194 100644
--- a/web/core/hooks/use-page-fallback.ts
+++ b/web/core/hooks/use-page-fallback.ts
@@ -1,6 +1,6 @@
import { useCallback, useEffect } from "react";
// plane editor
-import { EditorRefApi } from "@plane/editor";
+import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
// plane types
import { TDocumentPayload } from "@plane/types";
// hooks
@@ -29,7 +29,7 @@ export const usePageFallback = (args: TArgs) => {
editor.setProviderDocument(latestDecodedDescription);
const { binary, html, json } = editor.getDocument();
if (!binary || !json) return;
- const encodedBinary = Buffer.from(binary).toString("base64");
+ const encodedBinary = convertBinaryDataToBase64String(binary);
await updatePageDescription({
description_binary: encodedBinary,
From a7d8beedcaf832692338fc7d47b3471085fafa60 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Fri, 25 Oct 2024 15:23:14 +0530
Subject: [PATCH 04/21] chore: update yjs utils
---
live/src/core/helpers/document.ts | 16 ---
live/src/core/helpers/issue.ts | 33 -----
live/src/core/helpers/page.ts | 50 -------
live/src/core/lib/page.ts | 6 +-
live/src/server.ts | 23 +--
packages/editor/src/core/helpers/yjs-utils.ts | 132 ++++++++++++++++++
packages/editor/src/core/helpers/yjs.ts | 32 -----
packages/editor/src/index.ts | 2 +-
packages/editor/src/lib.ts | 2 +-
9 files changed, 150 insertions(+), 146 deletions(-)
delete mode 100644 live/src/core/helpers/document.ts
delete mode 100644 live/src/core/helpers/issue.ts
delete mode 100644 live/src/core/helpers/page.ts
create mode 100644 packages/editor/src/core/helpers/yjs-utils.ts
delete mode 100644 packages/editor/src/core/helpers/yjs.ts
diff --git a/live/src/core/helpers/document.ts b/live/src/core/helpers/document.ts
deleted file mode 100644
index 330f35879f7..00000000000
--- a/live/src/core/helpers/document.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as Y from "yjs";
-
-/**
- * @description apply updates to a document
- * @param {Uint8Array} document
- * @param {Uint8Array} updates
- * @returns {Uint8Array} conflicts resolved document
- */
-export const applyUpdatesToBinaryData = (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;
-};
diff --git a/live/src/core/helpers/issue.ts b/live/src/core/helpers/issue.ts
deleted file mode 100644
index 9d9194a9e49..00000000000
--- a/live/src/core/helpers/issue.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { getSchema } from "@tiptap/core";
-import { generateHTML } from "@tiptap/html";
-import { yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
-import * as Y from "yjs";
-// plane editor
-import { CoreEditorExtensionsWithoutProps } from "@plane/editor/lib";
-
-const RICH_TEXT_EDITOR_EXTENSIONS = CoreEditorExtensionsWithoutProps;
-const richTextEditorSchema = getSchema(RICH_TEXT_EDITOR_EXTENSIONS);
-
-export const getAllDocumentFormatsFromRichTextEditorBinaryData = (
- 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, richTextEditorSchema).toJSON();
- // convert to HTML
- const contentHTML = generateHTML(contentJSON, RICH_TEXT_EDITOR_EXTENSIONS);
-
- return {
- contentBinaryEncoded: base64Data,
- contentJSON,
- contentHTML,
- };
-};
diff --git a/live/src/core/helpers/page.ts b/live/src/core/helpers/page.ts
deleted file mode 100644
index f0db75f142b..00000000000
--- a/live/src/core/helpers/page.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-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 getAllDocumentFormatsFromDocumentEditorBinaryData = (
- 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 getBinaryDataFromDocumentEditorHTMLString = (
- descriptionHTML: string
-): {
- contentBinary: Uint8Array;
-} => {
- // convert HTML to JSON
- const contentJSON = generateJSON(descriptionHTML ?? "", 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,
- };
-};
diff --git a/live/src/core/lib/page.ts b/live/src/core/lib/page.ts
index 90eb229815d..fb80402aaf6 100644
--- a/live/src/core/lib/page.ts
+++ b/live/src/core/lib/page.ts
@@ -1,8 +1,8 @@
-// helpers
+// plane editor
import {
getAllDocumentFormatsFromDocumentEditorBinaryData,
getBinaryDataFromDocumentEditorHTMLString,
-} from "@/core/helpers/page.js";
+} from "@plane/editor/lib";
// services
import { PageService } from "@/core/services/page.service.js";
import { manualLogger } from "../helpers/logger.js";
@@ -48,7 +48,7 @@ const fetchDescriptionHTMLAndTransform = async (
try {
const pageDetails = await pageService.fetchDetails(workspaceSlug, projectId, pageId, cookie);
- const { contentBinary } = getBinaryDataFromDocumentEditorHTMLString(pageDetails.description_html ?? "");
+ const contentBinary = getBinaryDataFromDocumentEditorHTMLString(pageDetails.description_html ?? "");
return contentBinary;
} catch (error) {
manualLogger.error("Error while transforming from HTML to Uint8Array", error);
diff --git a/live/src/server.ts b/live/src/server.ts
index b1d2342a59a..195c8673b33 100644
--- a/live/src/server.ts
+++ b/live/src/server.ts
@@ -6,13 +6,16 @@ import * as Sentry from "@sentry/node";
import compression from "compression";
import helmet from "helmet";
import cors from "cors";
-import * as Y from "yjs";
+// plane editor
+import {
+ applyUpdates,
+ convertBase64StringToBinaryData,
+ getAllDocumentFormatsFromRichTextEditorBinaryData,
+} from "@plane/editor/lib";
// core hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// helpers
import { errorHandler } from "@/core/helpers/error-handler.js";
-import { applyUpdatesToBinaryData } from "@/core/helpers/document.js";
-import { getAllDocumentFormatsFromRichTextEditorBinaryData } from "@/core/helpers/issue.js";
import { logger, manualLogger } from "@/core/helpers/logger.js";
const app = express();
@@ -71,19 +74,19 @@ app.post("/resolve-document-conflicts", (req, res) => {
throw new Error("Missing required fields");
}
// convert from base64 to buffer
- const originalDocumentBuffer = original_document ? Buffer.from(original_document, "base64") : null;
- const updatesBuffer = updates ? Buffer.from(updates, "base64") : null;
+ 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) {
- const yDoc = new Y.Doc();
- Y.applyUpdate(yDoc, decodedUpdates);
- resolvedDocument = Y.encodeStateAsUpdate(yDoc);
+ // use updates to create the document id original_description is null
+ resolvedDocument = applyUpdates(decodedUpdates);
} else {
- resolvedDocument = applyUpdatesToBinaryData(decodedOriginalDocument, decodedUpdates);
+ // use original document and updates to resolve conflicts
+ resolvedDocument = applyUpdates(decodedOriginalDocument, decodedUpdates);
}
const { contentBinaryEncoded, contentHTML, contentJSON } =
@@ -95,7 +98,7 @@ app.post("/resolve-document-conflicts", (req, res) => {
description: contentJSON,
});
} catch (error) {
- console.log("error", error);
+ console.error("error", error);
res.status(500).send({
message: "Internal server error",
});
diff --git a/packages/editor/src/core/helpers/yjs-utils.ts b/packages/editor/src/core/helpers/yjs-utils.ts
new file mode 100644
index 00000000000..6426c97682f
--- /dev/null
+++ b/packages/editor/src/core/helpers/yjs-utils.ts
@@ -0,0 +1,132 @@
+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 {Buffer}
+ */
+export const convertBase64StringToBinaryData = (document: string): Buffer => 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 ?? "", 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 ?? "", 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,
+ };
+};
diff --git a/packages/editor/src/core/helpers/yjs.ts b/packages/editor/src/core/helpers/yjs.ts
deleted file mode 100644
index 40a35857199..00000000000
--- a/packages/editor/src/core/helpers/yjs.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { CoreEditorExtensionsWithoutProps } from "@/extensions";
-import { getSchema } from "@tiptap/core";
-import { generateJSON } from "@tiptap/html";
-import { prosemirrorJSONToYDoc } from "y-prosemirror";
-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;
-};
-
-const richTextEditorSchema = getSchema(CoreEditorExtensionsWithoutProps);
-export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: string): Uint8Array => {
- // convert HTML to JSON
- const contentJSON = generateJSON(descriptionHTML ?? "", CoreEditorExtensionsWithoutProps);
- // 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;
-};
diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts
index adb8c4c153d..ec420605f69 100644
--- a/packages/editor/src/index.ts
+++ b/packages/editor/src/index.ts
@@ -27,7 +27,7 @@ export * from "@/constants/common";
// helpers
export * from "@/helpers/common";
export * from "@/helpers/editor-commands";
-export * from "@/helpers/yjs";
+export * from "@/helpers/yjs-utils";
export * from "@/extensions/table/table";
// components
diff --git a/packages/editor/src/lib.ts b/packages/editor/src/lib.ts
index e14c40127fb..2f684724bc2 100644
--- a/packages/editor/src/lib.ts
+++ b/packages/editor/src/lib.ts
@@ -1 +1 @@
-export * from "@/extensions/core-without-props";
+export * from "@/helpers/yjs-utils";
From baf1517022f91554a8cdb91396a7b8801c539ffb Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Fri, 25 Oct 2024 22:35:42 +0530
Subject: [PATCH 05/21] chore: handle component re-mounting
---
.../use-collaborative-rich-text-editor.ts | 2 +
.../custom-collaboration-provider.ts | 4 +
.../components/inbox/content/issue-root.tsx | 54 +++--
.../components/issues/description-input.tsx | 2 +
.../issues/issue-detail/main-content.tsx | 1 +
.../issues/peek-overview/issue-detail.tsx | 1 +
yarn.lock | 200 +++++++++++++++++-
7 files changed, 230 insertions(+), 34 deletions(-)
diff --git a/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
index 17d0ab1aaf1..90b8b681872 100644
--- a/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
+++ b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
@@ -36,9 +36,11 @@ export const useCollaborativeRichTextEditor = (props: TCollaborativeRichTextEdit
);
useEffect(() => {
+ if (provider.hasSynced) return;
if (value.length > 0) {
Y.applyUpdate(provider.document, value);
}
+ provider.hasSynced = true;
}, [value, provider.document]);
const editor = useEditor({
diff --git a/packages/editor/src/core/providers/custom-collaboration-provider.ts b/packages/editor/src/core/providers/custom-collaboration-provider.ts
index 3375b58c5c2..2541cbd45b9 100644
--- a/packages/editor/src/core/providers/custom-collaboration-provider.ts
+++ b/packages/editor/src/core/providers/custom-collaboration-provider.ts
@@ -19,6 +19,8 @@ export type CollaborationProviderConfiguration = Required;
export class CustomCollaborationProvider {
+ public hasSynced: boolean;
+
public configuration: CompleteCollaboratorProviderConfiguration = {
name: "",
document: new Y.Doc(),
@@ -26,6 +28,7 @@ export class CustomCollaborationProvider {
};
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));
@@ -43,6 +46,7 @@ export class CustomCollaborationProvider {
}
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
diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx
index 044270bfc48..258a68cf709 100644
--- a/web/core/components/inbox/content/issue-root.tsx
+++ b/web/core/components/inbox/content/issue-root.tsx
@@ -6,7 +6,7 @@ import { usePathname } from "next/navigation";
// plane types
import { TIssue } from "@plane/types";
// plane ui
-import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
+import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { InboxIssueContentProperties } from "@/components/inbox/content";
import {
@@ -18,7 +18,7 @@ import {
IssueAttachmentRoot,
} from "@/components/issues";
// hooks
-import { useEventTracker, useProjectInbox, useUser } from "@/hooks/store";
+import { useEventTracker, useUser } from "@/hooks/store";
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
// services
import { InboxIssueService } from "@/services/inbox";
@@ -42,7 +42,6 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
const { data: currentUser } = useUser();
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
const { captureIssueEvent } = useEventTracker();
- const { loader } = useProjectInbox();
useEffect(() => {
if (isSubmitting === "submitted") {
@@ -98,7 +97,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
}
},
}),
- [inboxIssue]
+ [captureIssueEvent, inboxIssue, pathname]
);
if (!issue?.project_id || !issue?.id) return <>>;
@@ -118,32 +117,27 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
containerClassName="-ml-3"
/>
- {loader === "issue-loading" ? (
-
-
-
- ) : (
- "}
- disabled={!isEditable}
- fetchDescription={async () => {
- if (!workspaceSlug || !projectId || !issue.id) return;
- return await inboxIssueService.fetchDescriptionBinary(workspaceSlug, projectId, issue.id);
- }}
- updateDescription={async (data) => {
- if (!workspaceSlug || !projectId || !issue.id) return;
- return await inboxIssueService.updateDescriptionBinary(workspaceSlug, projectId, issue.id, {
- description_binary: data,
- });
- }}
- issueId={issue.id}
- issueOperations={issueOperations}
- projectId={issue.project_id}
- setIsSubmitting={(value) => setIsSubmitting(value)}
- workspaceSlug={workspaceSlug}
- />
- )}
+ "}
+ disabled={!isEditable}
+ fetchDescription={async () => {
+ if (!workspaceSlug || !projectId || !issue.id) return;
+ return await inboxIssueService.fetchDescriptionBinary(workspaceSlug, projectId, issue.id);
+ }}
+ updateDescription={async (data) => {
+ if (!workspaceSlug || !projectId || !issue.id) return;
+ return await inboxIssueService.updateDescriptionBinary(workspaceSlug, projectId, issue.id, {
+ description_binary: data,
+ });
+ }}
+ issueId={issue.id}
+ issueOperations={issueOperations}
+ projectId={issue.project_id}
+ setIsSubmitting={(value) => setIsSubmitting(value)}
+ workspaceSlug={workspaceSlug}
+ />
{currentUser && (
Promise;
issueId: string;
issueOperations: TIssueOperations;
+ key: string;
placeholder?: string | ((isFocused: boolean, value: string) => string);
projectId: string;
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
@@ -100,6 +101,7 @@ export const IssueDescriptionInput: FC = observer((p
<>
{!disabled ? (
{
diff --git a/web/core/components/issues/issue-detail/main-content.tsx b/web/core/components/issues/issue-detail/main-content.tsx
index 0a6bbd82191..0afa0243316 100644
--- a/web/core/components/issues/issue-detail/main-content.tsx
+++ b/web/core/components/issues/issue-detail/main-content.tsx
@@ -90,6 +90,7 @@ export const IssueMainContent: React.FC = observer((props) => {
/>
"}
disabled={!isEditable}
diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx
index 19bdd2e975b..4592f3ae9cd 100644
--- a/web/core/components/issues/peek-overview/issue-detail.tsx
+++ b/web/core/components/issues/peek-overview/issue-detail.tsx
@@ -76,6 +76,7 @@ export const PeekOverviewIssueDetails: FC = observer(
/>
"}
disabled={disabled}
diff --git a/yarn.lock b/yarn.lock
index 9dbe2e5ca10..e0e8dbaa98f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2941,10 +2941,10 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958"
integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==
-"@storybook/addon-actions@8.3.5":
- version "8.3.5"
- resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-8.3.5.tgz#03fdb891114439ed47cb7df6ef21826530449db7"
- integrity sha512-t8D5oo+4XfD+F8091wLa2y/CDd/W2lExCeol5Vm1tp5saO+u6f2/d7iykLhTowWV84Uohi3D073uFeyTAlGebg==
+"@storybook/addon-actions@8.3.6":
+ version "8.3.6"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-8.3.6.tgz#80c5dbfc2278d72dc461a954bb729165ee1dfecb"
+ integrity sha512-nOqgl0WoZK2KwjaABaXMoIgrIHOQl9inOzJvqQau0HOtsvnXGXYfJXYnpjZenoZDoZXKbUDl0U2haDFx2a2fJw==
dependencies:
"@storybook/global" "^5.0.0"
"@types/uuid" "^9.0.1"
@@ -3780,6 +3780,57 @@
dependencies:
"@types/node" "*"
+"@types/d3-array@^3.0.3":
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
+ integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
+
+"@types/d3-color@*":
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
+ integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-ease@^3.0.0":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
+ integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-interpolate@^3.0.1":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
+ integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-path@*":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a"
+ integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==
+
+"@types/d3-scale@^4.0.2":
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb"
+ integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==
+ dependencies:
+ "@types/d3-time" "*"
+
+"@types/d3-shape@^3.1.0":
+ version "3.1.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72"
+ integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==
+ dependencies:
+ "@types/d3-path" "*"
+
+"@types/d3-time@*", "@types/d3-time@^3.0.0":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be"
+ integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==
+
+"@types/d3-timer@^3.0.0":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
+ integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
"@types/debug@^4.0.0":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917"
@@ -5697,11 +5748,23 @@ d3-array@2, d3-array@^2.3.0:
dependencies:
internmap "^1.0.0"
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+ integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+ dependencies:
+ internmap "1 - 2"
+
"d3-color@1 - 2", d3-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e"
integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==
+"d3-color@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
d3-delaunay@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-5.3.0.tgz#b47f05c38f854a4e7b3cea80e0bb12e57398772d"
@@ -5709,11 +5772,21 @@ d3-delaunay@^5.3.0:
dependencies:
delaunator "4"
+d3-ease@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
"d3-format@1 - 2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767"
integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==
+"d3-format@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+ integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
d3-format@^1.4.4:
version "1.4.5"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
@@ -5726,11 +5799,23 @@ d3-format@^1.4.4:
dependencies:
d3-color "1 - 2"
+"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
d3-path@1:
version "1.0.9"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
+d3-path@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+ integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
d3-scale-chromatic@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz#c13f3af86685ff91323dc2f0ebd2dabbd72d8bab"
@@ -5750,6 +5835,17 @@ d3-scale@^3.2.3:
d3-time "^2.1.1"
d3-time-format "2 - 3"
+d3-scale@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+ integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+ dependencies:
+ d3-array "2.10.0 - 3"
+ d3-format "1 - 3"
+ d3-interpolate "1.2.0 - 3"
+ d3-time "2.1.1 - 3"
+ d3-time-format "2 - 4"
+
d3-shape@^1.3.5:
version "1.3.7"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
@@ -5757,6 +5853,13 @@ d3-shape@^1.3.5:
dependencies:
d3-path "1"
+d3-shape@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+ integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+ dependencies:
+ d3-path "^3.1.0"
+
"d3-time-format@2 - 3", d3-time-format@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6"
@@ -5764,6 +5867,13 @@ d3-shape@^1.3.5:
dependencies:
d3-time "1 - 2"
+"d3-time-format@2 - 4":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+ integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+ dependencies:
+ d3-time "1 - 3"
+
"d3-time@1 - 2", d3-time@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682"
@@ -5771,11 +5881,23 @@ d3-shape@^1.3.5:
dependencies:
d3-array "2"
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+ integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+ dependencies:
+ d3-array "2 - 3"
+
d3-time@^1.0.10, d3-time@^1.0.11:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
+d3-timer@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -5854,6 +5976,11 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
+decimal.js-light@^2.4.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
+ integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
+
decimal.js@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
@@ -6726,6 +6853,11 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+eventemitter3@^4.0.1:
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+ integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
events@^3.2.0, events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
@@ -6810,6 +6942,11 @@ fast-deep-equal@^3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+fast-equals@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d"
+ integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==
+
fast-fifo@^1.2.0, fast-fifo@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
@@ -7651,6 +7788,11 @@ internal-slot@^1.0.7:
hasown "^2.0.0"
side-channel "^1.0.4"
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
internmap@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
@@ -10328,6 +10470,15 @@ react-selecto@^1.25.0:
dependencies:
selecto "~1.26.3"
+react-smooth@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.1.tgz#6200d8699bfe051ae40ba187988323b1449eab1a"
+ integrity sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==
+ dependencies:
+ fast-equals "^5.0.1"
+ prop-types "^15.8.1"
+ react-transition-group "^4.4.5"
+
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
@@ -10411,6 +10562,27 @@ recast@^0.23.5:
tiny-invariant "^1.3.3"
tslib "^2.0.1"
+recharts-scale@^0.4.4:
+ version "0.4.5"
+ resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
+ integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
+ dependencies:
+ decimal.js-light "^2.4.1"
+
+recharts@^2.12.7:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.13.0.tgz#a293322ea357491393cc7ad6fcbb1e5f8e99bc93"
+ integrity sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==
+ dependencies:
+ clsx "^2.0.0"
+ eventemitter3 "^4.0.1"
+ lodash "^4.17.21"
+ react-is "^18.3.1"
+ react-smooth "^4.0.0"
+ recharts-scale "^0.4.4"
+ tiny-invariant "^1.3.1"
+ victory-vendor "^36.6.8"
+
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -12191,6 +12363,26 @@ vfile@^5.0.0:
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
+victory-vendor@^36.6.8:
+ version "36.9.2"
+ resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801"
+ integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==
+ dependencies:
+ "@types/d3-array" "^3.0.3"
+ "@types/d3-ease" "^3.0.0"
+ "@types/d3-interpolate" "^3.0.1"
+ "@types/d3-scale" "^4.0.2"
+ "@types/d3-shape" "^3.1.0"
+ "@types/d3-time" "^3.0.0"
+ "@types/d3-timer" "^3.0.0"
+ d3-array "^3.1.6"
+ d3-ease "^3.0.1"
+ d3-interpolate "^3.0.1"
+ d3-scale "^4.0.2"
+ d3-shape "^3.1.0"
+ d3-time "^3.0.0"
+ d3-timer "^3.0.1"
+
vite-compatible-readable-stream@^3.6.1:
version "3.6.1"
resolved "https://registry.yarnpkg.com/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz#27267aebbdc9893c0ddf65a421279cbb1e31d8cd"
From 54ecea1911c776d966440047802216b2e6ef7dbb Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Fri, 25 Oct 2024 22:44:54 +0530
Subject: [PATCH 06/21] chore: update buffer response type
---
.../core/hooks/use-collaborative-rich-text-editor.ts | 2 +-
.../core/providers/custom-collaboration-provider.ts | 10 +++++-----
.../editor/rich-text-editor/collaborative-editor.tsx | 1 +
web/core/components/inbox/content/issue-root.tsx | 8 ++++++--
web/core/components/issues/description-input.tsx | 4 ++--
.../components/issues/issue-detail/main-content.tsx | 8 ++++++--
.../components/issues/peek-overview/issue-detail.tsx | 8 ++++++--
web/core/components/pages/editor/page-root.tsx | 4 +++-
web/core/hooks/use-issue-description.ts | 4 ++--
web/core/hooks/use-page-fallback.ts | 2 +-
web/core/services/inbox/inbox-issue.service.ts | 4 ++--
web/core/services/issue/issue.service.ts | 4 ++--
web/core/services/page/project-page.service.ts | 2 +-
13 files changed, 38 insertions(+), 23 deletions(-)
diff --git a/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
index 90b8b681872..af85add74ee 100644
--- a/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
+++ b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
@@ -37,7 +37,7 @@ export const useCollaborativeRichTextEditor = (props: TCollaborativeRichTextEdit
useEffect(() => {
if (provider.hasSynced) return;
- if (value.length > 0) {
+ if (value && value.length > 0) {
Y.applyUpdate(provider.document, value);
}
provider.hasSynced = true;
diff --git a/packages/editor/src/core/providers/custom-collaboration-provider.ts b/packages/editor/src/core/providers/custom-collaboration-provider.ts
index 2541cbd45b9..036b15fa152 100644
--- a/packages/editor/src/core/providers/custom-collaboration-provider.ts
+++ b/packages/editor/src/core/providers/custom-collaboration-provider.ts
@@ -1,6 +1,6 @@
import * as Y from "yjs";
-export interface CompleteCollaboratorProviderConfiguration {
+export interface CompleteCollaborationProviderConfiguration {
/**
* The identifier/name of your document
*/
@@ -15,13 +15,13 @@ export interface CompleteCollaboratorProviderConfiguration {
onChange: (updates: Uint8Array) => void;
}
-export type CollaborationProviderConfiguration = Required> &
- Partial;
+export type CollaborationProviderConfiguration = Required> &
+ Partial;
export class CustomCollaborationProvider {
public hasSynced: boolean;
- public configuration: CompleteCollaboratorProviderConfiguration = {
+ public configuration: CompleteCollaborationProviderConfiguration = {
name: "",
document: new Y.Doc(),
onChange: () => {},
@@ -34,7 +34,7 @@ export class CustomCollaborationProvider {
this.document.on("destroy", this.documentDestroyHandler.bind(this));
}
- public setConfiguration(configuration: Partial = {}): void {
+ public setConfiguration(configuration: Partial = {}): void {
this.configuration = {
...this.configuration,
...configuration,
diff --git a/web/core/components/editor/rich-text-editor/collaborative-editor.tsx b/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
index 6f4dd15e2e5..99c4253e29e 100644
--- a/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
+++ b/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
@@ -12,6 +12,7 @@ import { useMember, useMention, useUser } from "@/hooks/store";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
interface Props extends Omit {
+ key: string;
projectId: string;
uploadFile: (file: File) => Promise;
workspaceId: string;
diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx
index 258a68cf709..36d181af721 100644
--- a/web/core/components/inbox/content/issue-root.tsx
+++ b/web/core/components/inbox/content/issue-root.tsx
@@ -123,11 +123,15 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
descriptionHTML={issue.description_html ?? ""}
disabled={!isEditable}
fetchDescription={async () => {
- if (!workspaceSlug || !projectId || !issue.id) return;
+ if (!workspaceSlug || !projectId || !issue.id) {
+ throw new Error("Required fields missing while fetching binary description");
+ }
return await inboxIssueService.fetchDescriptionBinary(workspaceSlug, projectId, issue.id);
}}
updateDescription={async (data) => {
- if (!workspaceSlug || !projectId || !issue.id) return;
+ if (!workspaceSlug || !projectId || !issue.id) {
+ throw new Error("Required fields missing while updating binary description");
+ }
return await inboxIssueService.updateDescriptionBinary(workspaceSlug, projectId, issue.id, {
description_binary: data,
});
diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx
index 1798efe8cbe..761059e2a4c 100644
--- a/web/core/components/issues/description-input.tsx
+++ b/web/core/components/issues/description-input.tsx
@@ -25,14 +25,14 @@ export type IssueDescriptionInputProps = {
containerClassName?: string;
descriptionHTML: string;
disabled?: boolean;
- fetchDescription: () => Promise;
+ fetchDescription: () => Promise;
issueId: string;
issueOperations: TIssueOperations;
key: string;
placeholder?: string | ((isFocused: boolean, value: string) => string);
projectId: string;
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
- updateDescription: (data: string) => Promise;
+ updateDescription: (data: string) => Promise;
workspaceSlug: string;
};
diff --git a/web/core/components/issues/issue-detail/main-content.tsx b/web/core/components/issues/issue-detail/main-content.tsx
index 0afa0243316..a96d4f4bd3a 100644
--- a/web/core/components/issues/issue-detail/main-content.tsx
+++ b/web/core/components/issues/issue-detail/main-content.tsx
@@ -95,11 +95,15 @@ export const IssueMainContent: React.FC = observer((props) => {
descriptionHTML={issue.description_html ?? ""}
disabled={!isEditable}
fetchDescription={async () => {
- if (!workspaceSlug || !projectId || !issueId) return;
+ if (!workspaceSlug || !projectId || !issueId) {
+ throw new Error("Required fields missing while fetching binary description");
+ }
return await issueService.fetchDescriptionBinary(workspaceSlug, projectId, issueId);
}}
updateDescription={async (data) => {
- if (!workspaceSlug || !issue.project_id || !issue.id) return;
+ if (!workspaceSlug || !issue.project_id || !issue.id) {
+ throw new Error("Required fields missing while updating binary description");
+ }
return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
description_binary: data,
});
diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx
index 4592f3ae9cd..5d1ba58f72d 100644
--- a/web/core/components/issues/peek-overview/issue-detail.tsx
+++ b/web/core/components/issues/peek-overview/issue-detail.tsx
@@ -81,11 +81,15 @@ export const PeekOverviewIssueDetails: FC = observer(
descriptionHTML={issue.description_html ?? ""}
disabled={disabled}
fetchDescription={async () => {
- if (!workspaceSlug || !issue.project_id || !issue.id) return;
+ if (!workspaceSlug || !issue.project_id || !issue.id) {
+ throw new Error("Required fields missing while fetching binary description");
+ }
return await issueService.fetchDescriptionBinary(workspaceSlug, issue.project_id, issue.id);
}}
updateDescription={async (data) => {
- if (!workspaceSlug || !issue.project_id || !issue.id) return;
+ if (!workspaceSlug || !issue.project_id || !issue.id) {
+ throw new Error("Required fields missing while updating binary description");
+ }
return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
description_binary: data,
});
diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx
index ff1f3519e93..500a77586f9 100644
--- a/web/core/components/pages/editor/page-root.tsx
+++ b/web/core/components/pages/editor/page-root.tsx
@@ -50,7 +50,9 @@ export const PageRoot = observer((props: TPageRootProps) => {
usePageFallback({
editorRef,
fetchPageDescription: async () => {
- if (!page.id) return;
+ if (!page.id) {
+ throw new Error("Required fields missing while fetching binary description");
+ }
return await projectPageService.fetchDescriptionBinary(workspaceSlug, projectId, page.id);
},
hasConnectionFailed,
diff --git a/web/core/hooks/use-issue-description.ts b/web/core/hooks/use-issue-description.ts
index ba2c5a9d360..b29cb714ce0 100644
--- a/web/core/hooks/use-issue-description.ts
+++ b/web/core/hooks/use-issue-description.ts
@@ -4,9 +4,9 @@ import { EditorRefApi, getBinaryDataFromRichTextEditorHTMLString } from "@plane/
type TArgs = {
descriptionHTML: string | null;
- fetchDescription: () => Promise;
+ fetchDescription: () => Promise;
id: string;
- updateDescription?: (data: string) => Promise;
+ updateDescription?: (data: string) => Promise;
};
export const useIssueDescription = (args: TArgs) => {
diff --git a/web/core/hooks/use-page-fallback.ts b/web/core/hooks/use-page-fallback.ts
index d07aac0d194..50335e1e0e4 100644
--- a/web/core/hooks/use-page-fallback.ts
+++ b/web/core/hooks/use-page-fallback.ts
@@ -8,7 +8,7 @@ import useAutoSave from "@/hooks/use-auto-save";
type TArgs = {
editorRef: React.RefObject;
- fetchPageDescription: () => Promise;
+ fetchPageDescription: () => Promise;
hasConnectionFailed: boolean;
updatePageDescription: (data: TDocumentPayload) => Promise;
};
diff --git a/web/core/services/inbox/inbox-issue.service.ts b/web/core/services/inbox/inbox-issue.service.ts
index 3ac13bce6d2..f170fcd9a73 100644
--- a/web/core/services/inbox/inbox-issue.service.ts
+++ b/web/core/services/inbox/inbox-issue.service.ts
@@ -76,7 +76,7 @@ export class InboxIssueService extends APIService {
});
}
- async fetchDescriptionBinary(workspaceSlug: string, projectId: string, inboxIssueId: string): Promise {
+ async fetchDescriptionBinary(workspaceSlug: string, projectId: string, inboxIssueId: string): Promise {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inbox-issues/${inboxIssueId}/description/`,
{
@@ -97,7 +97,7 @@ export class InboxIssueService extends APIService {
projectId: string,
inboxIssueId: string,
data: Pick
- ): Promise {
+ ): Promise {
return this.post(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inbox-issues/${inboxIssueId}/description/`,
data,
diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts
index 0fd8d1f678d..8f6a0d0da93 100644
--- a/web/core/services/issue/issue.service.ts
+++ b/web/core/services/issue/issue.service.ts
@@ -373,7 +373,7 @@ export class IssueService extends APIService {
});
}
- async fetchDescriptionBinary(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ async fetchDescriptionBinary(workspaceSlug: string, projectId: string, issueId: string): Promise {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/description/`, {
headers: {
"Content-Type": "application/octet-stream",
@@ -391,7 +391,7 @@ export class IssueService extends APIService {
projectId: string,
issueId: string,
data: Pick
- ): Promise {
+ ): Promise {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/description/`, data, {
responseType: "arraybuffer",
})
diff --git a/web/core/services/page/project-page.service.ts b/web/core/services/page/project-page.service.ts
index e2f22d5ad3c..3e963e9661c 100644
--- a/web/core/services/page/project-page.service.ts
+++ b/web/core/services/page/project-page.service.ts
@@ -128,7 +128,7 @@ export class ProjectPageService extends APIService {
});
}
- async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise {
+ async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
headers: {
"Content-Type": "application/octet-stream",
From fcd06fc35c7b531f1389c24619da93ea931f33ce Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Fri, 25 Oct 2024 22:50:49 +0530
Subject: [PATCH 07/21] chore: add try catch for issue description update
---
web/core/hooks/use-issue-description.ts | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/web/core/hooks/use-issue-description.ts b/web/core/hooks/use-issue-description.ts
index b29cb714ce0..5282856c866 100644
--- a/web/core/hooks/use-issue-description.ts
+++ b/web/core/hooks/use-issue-description.ts
@@ -17,11 +17,15 @@ export const useIssueDescription = (args: TArgs) => {
const resolveConflictsAndUpdateDescription = useCallback(
async (encodedDescription: string, editorRef: EditorRefApi | null) => {
if (!updateDescription) return;
- const conflictFreeEncodedDescription = await updateDescription(encodedDescription);
- const decodedDescription = conflictFreeEncodedDescription
- ? new Uint8Array(conflictFreeEncodedDescription)
- : new Uint8Array();
- editorRef?.setProviderDocument(decodedDescription);
+ 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]
);
From 547fc86f175c13d69bed19010fcac15c4d260d32 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Fri, 25 Oct 2024 22:54:58 +0530
Subject: [PATCH 08/21] chore: update buffer response type
---
.../src/core/hooks/use-collaborative-rich-text-editor.ts | 6 +++++-
web/core/components/issues/description-input.tsx | 4 ++--
web/core/hooks/use-issue-description.ts | 4 ++--
web/core/hooks/use-page-fallback.ts | 2 +-
web/core/services/inbox/inbox-issue.service.ts | 4 ++--
web/core/services/issue/issue.service.ts | 4 ++--
web/core/services/page/project-page.service.ts | 2 +-
7 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
index af85add74ee..3bffc5b2983 100644
--- a/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
+++ b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
@@ -38,7 +38,11 @@ export const useCollaborativeRichTextEditor = (props: TCollaborativeRichTextEdit
useEffect(() => {
if (provider.hasSynced) return;
if (value && value.length > 0) {
- Y.applyUpdate(provider.document, value);
+ try {
+ Y.applyUpdate(provider.document, value);
+ } catch (error) {
+ console.error("Error applying binary updates to the description", error);
+ }
}
provider.hasSynced = true;
}, [value, provider.document]);
diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx
index 761059e2a4c..d13a76f00f1 100644
--- a/web/core/components/issues/description-input.tsx
+++ b/web/core/components/issues/description-input.tsx
@@ -25,14 +25,14 @@ export type IssueDescriptionInputProps = {
containerClassName?: string;
descriptionHTML: string;
disabled?: boolean;
- fetchDescription: () => Promise;
+ fetchDescription: () => Promise;
issueId: string;
issueOperations: TIssueOperations;
key: string;
placeholder?: string | ((isFocused: boolean, value: string) => string);
projectId: string;
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
- updateDescription: (data: string) => Promise;
+ updateDescription: (data: string) => Promise;
workspaceSlug: string;
};
diff --git a/web/core/hooks/use-issue-description.ts b/web/core/hooks/use-issue-description.ts
index 5282856c866..1ffaea3b152 100644
--- a/web/core/hooks/use-issue-description.ts
+++ b/web/core/hooks/use-issue-description.ts
@@ -4,9 +4,9 @@ import { EditorRefApi, getBinaryDataFromRichTextEditorHTMLString } from "@plane/
type TArgs = {
descriptionHTML: string | null;
- fetchDescription: () => Promise;
+ fetchDescription: () => Promise;
id: string;
- updateDescription?: (data: string) => Promise;
+ updateDescription?: (data: string) => Promise;
};
export const useIssueDescription = (args: TArgs) => {
diff --git a/web/core/hooks/use-page-fallback.ts b/web/core/hooks/use-page-fallback.ts
index 50335e1e0e4..4e604ffb466 100644
--- a/web/core/hooks/use-page-fallback.ts
+++ b/web/core/hooks/use-page-fallback.ts
@@ -8,7 +8,7 @@ import useAutoSave from "@/hooks/use-auto-save";
type TArgs = {
editorRef: React.RefObject;
- fetchPageDescription: () => Promise;
+ fetchPageDescription: () => Promise;
hasConnectionFailed: boolean;
updatePageDescription: (data: TDocumentPayload) => Promise;
};
diff --git a/web/core/services/inbox/inbox-issue.service.ts b/web/core/services/inbox/inbox-issue.service.ts
index f170fcd9a73..690abda0b83 100644
--- a/web/core/services/inbox/inbox-issue.service.ts
+++ b/web/core/services/inbox/inbox-issue.service.ts
@@ -76,7 +76,7 @@ export class InboxIssueService extends APIService {
});
}
- async fetchDescriptionBinary(workspaceSlug: string, projectId: string, inboxIssueId: string): Promise {
+ async fetchDescriptionBinary(workspaceSlug: string, projectId: string, inboxIssueId: string): Promise {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inbox-issues/${inboxIssueId}/description/`,
{
@@ -97,7 +97,7 @@ export class InboxIssueService extends APIService {
projectId: string,
inboxIssueId: string,
data: Pick
- ): Promise {
+ ): Promise {
return this.post(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inbox-issues/${inboxIssueId}/description/`,
data,
diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts
index 8f6a0d0da93..2ffa8030bff 100644
--- a/web/core/services/issue/issue.service.ts
+++ b/web/core/services/issue/issue.service.ts
@@ -373,7 +373,7 @@ export class IssueService extends APIService {
});
}
- async fetchDescriptionBinary(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ async fetchDescriptionBinary(workspaceSlug: string, projectId: string, issueId: string): Promise {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/description/`, {
headers: {
"Content-Type": "application/octet-stream",
@@ -391,7 +391,7 @@ export class IssueService extends APIService {
projectId: string,
issueId: string,
data: Pick
- ): Promise {
+ ): Promise {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/description/`, data, {
responseType: "arraybuffer",
})
diff --git a/web/core/services/page/project-page.service.ts b/web/core/services/page/project-page.service.ts
index 3e963e9661c..f5331d8891f 100644
--- a/web/core/services/page/project-page.service.ts
+++ b/web/core/services/page/project-page.service.ts
@@ -128,7 +128,7 @@ export class ProjectPageService extends APIService {
});
}
- async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise {
+ async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
headers: {
"Content-Type": "application/octet-stream",
From 7448c5b37e1c11efbc596252969d0a830efa9263 Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Tue, 5 Nov 2024 14:54:36 +0530
Subject: [PATCH 09/21] chore: description binary in retrieve
---
apiserver/plane/app/serializers/draft.py | 2 ++
apiserver/plane/app/serializers/issue.py | 2 ++
apiserver/plane/app/urls/issue.py | 1 -
3 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/apiserver/plane/app/serializers/draft.py b/apiserver/plane/app/serializers/draft.py
index e07e416a75d..b1df79f28e7 100644
--- a/apiserver/plane/app/serializers/draft.py
+++ b/apiserver/plane/app/serializers/draft.py
@@ -284,9 +284,11 @@ class Meta:
class DraftIssueDetailSerializer(DraftIssueSerializer):
description_html = serializers.CharField()
+ description_binary = serializers.CharField()
class Meta(DraftIssueSerializer.Meta):
fields = DraftIssueSerializer.Meta.fields + [
"description_html",
+ "description_binary",
]
read_only_fields = fields
diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py
index 2323c248a86..9762a505499 100644
--- a/apiserver/plane/app/serializers/issue.py
+++ b/apiserver/plane/app/serializers/issue.py
@@ -732,12 +732,14 @@ class Meta:
class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
+ description_binary = serializers.CharField()
is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + [
"description_html",
"is_subscribed",
+ "description_binary",
]
read_only_fields = fields
diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py
index 53c6f7dd5c5..4299c42bd19 100644
--- a/apiserver/plane/app/urls/issue.py
+++ b/apiserver/plane/app/urls/issue.py
@@ -295,7 +295,6 @@
IssueArchiveViewSet.as_view(
{
"get": "retrieve_description",
- "post": "update_description",
}
),
name="archive-issue-description",
From b3b1088e2dbe14fd0af2f07cbc5d8ba9960a77cf Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Fri, 8 Nov 2024 17:53:00 +0530
Subject: [PATCH 10/21] chore: update issue description hook
---
packages/editor/src/core/helpers/yjs-utils.ts | 4 +-
packages/types/src/issues/issue.d.ts | 1 +
.../components/issues/description-input.tsx | 4 +-
.../issues/peek-overview/issue-detail.tsx | 56 ++++++++++---------
web/core/hooks/use-issue-description.ts | 47 +++++++++-------
.../store/issue/issue-details/issue.store.ts | 3 +-
6 files changed, 64 insertions(+), 51 deletions(-)
diff --git a/packages/editor/src/core/helpers/yjs-utils.ts b/packages/editor/src/core/helpers/yjs-utils.ts
index 6426c97682f..dd1b9f2fa2f 100644
--- a/packages/editor/src/core/helpers/yjs-utils.ts
+++ b/packages/editor/src/core/helpers/yjs-utils.ts
@@ -39,9 +39,9 @@ export const convertBinaryDataToBase64String = (document: Uint8Array): string =>
/**
* @description this function decodes base64 string to binary data
* @param {string} document
- * @returns {Buffer}
+ * @returns {ArrayBuffer}
*/
-export const convertBase64StringToBinaryData = (document: string): Buffer => Buffer.from(document, "base64");
+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
diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts
index ae4a98d63f0..1d9580fd1b4 100644
--- a/packages/types/src/issues/issue.d.ts
+++ b/packages/types/src/issues/issue.d.ts
@@ -50,6 +50,7 @@ export type IssueRelation = {
};
export type TIssue = TBaseIssue & {
+ description_binary?: string;
description_html?: string;
is_subscribed?: boolean;
parent?: Partial;
diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx
index d13a76f00f1..e5215eeeba6 100644
--- a/web/core/components/issues/description-input.tsx
+++ b/web/core/components/issues/description-input.tsx
@@ -23,6 +23,7 @@ const fileService = new FileService();
export type IssueDescriptionInputProps = {
containerClassName?: string;
+ descriptionBinary: string | null;
descriptionHTML: string;
disabled?: boolean;
fetchDescription: () => Promise;
@@ -39,6 +40,7 @@ export type IssueDescriptionInputProps = {
export const IssueDescriptionInput: FC = observer((props) => {
const {
containerClassName,
+ descriptionBinary: savedDescriptionBinary,
descriptionHTML,
disabled,
fetchDescription,
@@ -57,9 +59,9 @@ export const IssueDescriptionInput: FC = observer((p
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString() ?? "";
// use issue description
const { descriptionBinary, resolveConflictsAndUpdateDescription } = useIssueDescription({
+ descriptionBinary: savedDescriptionBinary,
descriptionHTML,
id: issueId,
- fetchDescription,
updateDescription,
});
diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx
index 6e45e002bd0..bf8321abfdd 100644
--- a/web/core/components/issues/peek-overview/issue-detail.tsx
+++ b/web/core/components/issues/peek-overview/issue-detail.tsx
@@ -12,11 +12,12 @@ import useReloadConfirmations from "@/hooks/use-reload-confirmation";
// plane web components
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
+// plane web hooks
+import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
// services
import { IssueService } from "@/services/issue";
const issueService = new IssueService();
// local components
-import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
import { IssueDescriptionInput } from "../description-input";
import { IssueReaction } from "../issue-detail/reactions";
import { IssueTitleInput } from "../title-input";
@@ -101,31 +102,34 @@ export const PeekOverviewIssueDetails: FC = observer(
containerClassName="-ml-3"
/>
- "}
- disabled={disabled}
- fetchDescription={async () => {
- if (!workspaceSlug || !issue.project_id || !issue.id) {
- throw new Error("Required fields missing while fetching binary description");
- }
- return await issueService.fetchDescriptionBinary(workspaceSlug, issue.project_id, issue.id);
- }}
- updateDescription={async (data) => {
- if (!workspaceSlug || !issue.project_id || !issue.id) {
- throw new Error("Required fields missing while updating binary description");
- }
- return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
- description_binary: data,
- });
- }}
- issueId={issue.id}
- issueOperations={issueOperations}
- projectId={issue.project_id}
- setIsSubmitting={(value) => setIsSubmitting(value)}
- workspaceSlug={workspaceSlug}
- />
+ {issue.description_binary !== undefined && (
+ "}
+ disabled={disabled}
+ fetchDescription={async () => {
+ if (!workspaceSlug || !issue.project_id || !issue.id) {
+ throw new Error("Required fields missing while fetching binary description");
+ }
+ return await issueService.fetchDescriptionBinary(workspaceSlug, issue.project_id, issue.id);
+ }}
+ updateDescription={async (data) => {
+ if (!workspaceSlug || !issue.project_id || !issue.id) {
+ throw new Error("Required fields missing while updating binary description");
+ }
+ return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
+ description_binary: data,
+ });
+ }}
+ issueId={issue.id}
+ issueOperations={issueOperations}
+ projectId={issue.project_id}
+ setIsSubmitting={(value) => setIsSubmitting(value)}
+ workspaceSlug={workspaceSlug}
+ />
+ )}
{currentUser && (
Promise;
id: string;
updateDescription?: (data: string) => Promise;
};
export const useIssueDescription = (args: TArgs) => {
- const { descriptionHTML, fetchDescription, id, updateDescription } = args;
+ const { descriptionBinary: savedDescriptionBinary, descriptionHTML, id, updateDescription } = args;
// states
const [descriptionBinary, setDescriptionBinary] = useState(null);
// update description
@@ -32,25 +36,26 @@ export const useIssueDescription = (args: TArgs) => {
useEffect(() => {
if (descriptionBinary) return;
- // fetch latest binary description
- const fetchDecodedDescription = async () => {
- const encodedDescription = await fetchDescription();
- let decodedDescription = encodedDescription ? new Uint8Array(encodedDescription) : new Uint8Array();
- // if there's no binary data present, convert existing HTML string to binary
- if (decodedDescription.length === 0) {
- decodedDescription = getBinaryDataFromRichTextEditorHTMLString(descriptionHTML ?? "");
- } else {
- // decode binary string
- decodedDescription = new Uint8Array(encodedDescription);
- }
- setDescriptionBinary(decodedDescription);
- };
- fetchDecodedDescription();
- }, [descriptionBinary, descriptionHTML, fetchDescription]);
+ if (savedDescriptionBinary) {
+ const savedDescriptionBuffer = convertBase64StringToBinaryData(savedDescriptionBinary);
+ console.log("Saved", savedDescriptionBuffer);
+ const decodedSavedDescription = savedDescriptionBuffer
+ ? new Uint8Array(savedDescriptionBuffer)
+ : new Uint8Array();
+ setDescriptionBinary(decodedSavedDescription);
+ } else {
+ console.log("HTML");
+ const decodedDescriptionHTML = getBinaryDataFromRichTextEditorHTMLString(descriptionHTML ?? "");
+ setDescriptionBinary(decodedDescriptionHTML);
+ }
+ }, [descriptionBinary, descriptionHTML, savedDescriptionBinary]);
- useEffect(() => {
- setDescriptionBinary(null);
- }, [id]);
+ // useEffect(() => {
+ // console.log("Setting to null");
+ // setDescriptionBinary(null);
+ // }, [id]);
+
+ console.log("descriptionBinary", descriptionBinary);
return {
descriptionBinary,
diff --git a/web/core/store/issue/issue-details/issue.store.ts b/web/core/store/issue/issue-details/issue.store.ts
index db0ccc39af2..6d1d653d27d 100644
--- a/web/core/store/issue/issue-details/issue.store.ts
+++ b/web/core/store/issue/issue-details/issue.store.ts
@@ -15,7 +15,7 @@ export interface IIssueStoreActions {
workspaceSlug: string,
projectId: string,
issueId: string,
- issueStatus?: "DEFAULT" | "DRAFT",
+ issueStatus?: "DEFAULT" | "DRAFT"
) => Promise;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise;
@@ -156,6 +156,7 @@ export class IssueStore implements IIssueStore {
id: issue?.id,
sequence_id: issue?.sequence_id,
name: issue?.name,
+ description_binary: issue?.description_binary,
description_html: issue?.description_html,
sort_order: issue?.sort_order,
state_id: issue?.state_id,
From ccad0e7c79ba93de9566db80cc06859b74e39281 Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Fri, 8 Nov 2024 19:13:35 +0530
Subject: [PATCH 11/21] chore: decode description binary
---
apiserver/plane/app/serializers/issue.py | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py
index 9762a505499..bf2c6b4a780 100644
--- a/apiserver/plane/app/serializers/issue.py
+++ b/apiserver/plane/app/serializers/issue.py
@@ -1,3 +1,6 @@
+# Python imports
+import base64
+
# Django imports
from django.utils import timezone
from django.core.validators import URLValidator
@@ -730,9 +733,24 @@ class Meta:
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):
description_html = serializers.CharField()
- description_binary = serializers.CharField()
+ description_binary = Base64BinaryField()
is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta):
From 389ee74ff533fcef65666c443cdaf206e90c8a7e Mon Sep 17 00:00:00 2001
From: sriram veeraghanta
Date: Fri, 8 Nov 2024 19:23:42 +0530
Subject: [PATCH 12/21] chore: migrations fixes and cleanup
---
apiserver/plane/app/serializers/__init__.py | 1 -
apiserver/plane/app/serializers/workspace.py | 53 -------------
apiserver/plane/app/urls/project.py | 6 --
apiserver/plane/app/urls/workspace.py | 23 ------
apiserver/plane/app/views/__init__.py | 2 -
apiserver/plane/app/views/project/member.py | 49 ------------
apiserver/plane/app/views/workspace/member.py | 63 +---------------
...ter_teammember_unique_together_and_more.py | 74 +++++++++++++++++++
apiserver/plane/db/models/__init__.py | 2 -
apiserver/plane/db/models/page.py | 30 --------
apiserver/plane/db/models/workspace.py | 65 ----------------
11 files changed, 75 insertions(+), 293 deletions(-)
create mode 100644 apiserver/plane/db/migrations/0085_alter_teammember_unique_together_and_more.py
diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py
index 2e2c91baa1f..6c9e1fed17a 100644
--- a/apiserver/plane/app/serializers/__init__.py
+++ b/apiserver/plane/app/serializers/__init__.py
@@ -13,7 +13,6 @@
from .workspace import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
- TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py
index 1a2b89bba61..ce1c75dbf53 100644
--- a/apiserver/plane/app/serializers/workspace.py
+++ b/apiserver/plane/app/serializers/workspace.py
@@ -9,8 +9,6 @@
User,
Workspace,
WorkspaceMember,
- Team,
- TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
@@ -99,57 +97,6 @@ class Meta:
"updated_at",
]
-
-class TeamSerializer(BaseSerializer):
- members_detail = UserLiteSerializer(
- read_only=True, source="members", many=True
- )
- members = serializers.ListField(
- child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
- write_only=True,
- required=False,
- )
-
- class Meta:
- model = Team
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
- def create(self, validated_data, **kwargs):
- if "members" in validated_data:
- members = validated_data.pop("members")
- workspace = self.context["workspace"]
- team = Team.objects.create(**validated_data, workspace=workspace)
- team_members = [
- TeamMember(member=member, team=team, workspace=workspace)
- for member in members
- ]
- TeamMember.objects.bulk_create(team_members, batch_size=10)
- return team
- team = Team.objects.create(**validated_data)
- return team
-
- def update(self, instance, validated_data):
- if "members" in validated_data:
- members = validated_data.pop("members")
- TeamMember.objects.filter(team=instance).delete()
- team_members = [
- TeamMember(
- member=member, team=instance, workspace=instance.workspace
- )
- for member in members
- ]
- TeamMember.objects.bulk_create(team_members, batch_size=10)
- return super().update(instance, validated_data)
- return super().update(instance, validated_data)
-
-
class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
diff --git a/apiserver/plane/app/urls/project.py b/apiserver/plane/app/urls/project.py
index 0807c7616a2..4ea4522494d 100644
--- a/apiserver/plane/app/urls/project.py
+++ b/apiserver/plane/app/urls/project.py
@@ -7,7 +7,6 @@
ProjectMemberViewSet,
ProjectMemberUserEndpoint,
ProjectJoinEndpoint,
- AddTeamToProjectEndpoint,
ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
@@ -116,11 +115,6 @@
),
name="project-member",
),
- path(
- "workspaces//projects//team-invite/",
- AddTeamToProjectEndpoint.as_view(),
- name="projects",
- ),
path(
"workspaces//projects//project-views/",
ProjectUserViewsEndpoint.as_view(),
diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py
index fb6f4c13acc..6bee2523883 100644
--- a/apiserver/plane/app/urls/workspace.py
+++ b/apiserver/plane/app/urls/workspace.py
@@ -10,7 +10,6 @@
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
WorkSpaceAvailabilityCheckEndpoint,
- TeamMemberViewSet,
UserLastProjectWithWorkspaceEndpoint,
WorkspaceThemeViewSet,
WorkspaceUserProfileStatsEndpoint,
@@ -127,28 +126,6 @@
),
name="leave-workspace-members",
),
- path(
- "workspaces//teams/",
- TeamMemberViewSet.as_view(
- {
- "get": "list",
- "post": "create",
- }
- ),
- name="workspace-team-members",
- ),
- path(
- "workspaces//teams//",
- TeamMemberViewSet.as_view(
- {
- "put": "update",
- "patch": "partial_update",
- "delete": "destroy",
- "get": "retrieve",
- }
- ),
- name="workspace-team-members",
- ),
path(
"users/last-visited-workspace/",
UserLastProjectWithWorkspaceEndpoint.as_view(),
diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py
index 159686fa3ac..e67b9d2efb7 100644
--- a/apiserver/plane/app/views/__init__.py
+++ b/apiserver/plane/app/views/__init__.py
@@ -16,7 +16,6 @@
from .project.member import (
ProjectMemberViewSet,
- AddTeamToProjectEndpoint,
ProjectMemberUserEndpoint,
UserProjectRolesEndpoint,
)
@@ -49,7 +48,6 @@
from .workspace.member import (
WorkSpaceMemberViewSet,
- TeamMemberViewSet,
WorkspaceMemberUserEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceMemberUserViewsEndpoint,
diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py
index ccb5e75216e..36b2d3076c2 100644
--- a/apiserver/plane/app/views/project/member.py
+++ b/apiserver/plane/app/views/project/member.py
@@ -21,7 +21,6 @@
Project,
ProjectMember,
Workspace,
- TeamMember,
IssueUserProperty,
WorkspaceMember,
)
@@ -342,54 +341,6 @@ def leave(self, request, slug, project_id):
return Response(status=status.HTTP_204_NO_CONTENT)
-class AddTeamToProjectEndpoint(BaseAPIView):
- permission_classes = [
- ProjectBasePermission,
- ]
-
- def post(self, request, slug, project_id):
- team_members = TeamMember.objects.filter(
- workspace__slug=slug, team__in=request.data.get("teams", [])
- ).values_list("member", flat=True)
-
- if len(team_members) == 0:
- return Response(
- {"error": "No such team exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- workspace = Workspace.objects.get(slug=slug)
-
- project_members = []
- issue_props = []
- for member in team_members:
- project_members.append(
- ProjectMember(
- project_id=project_id,
- member_id=member,
- workspace=workspace,
- created_by=request.user,
- )
- )
- issue_props.append(
- IssueUserProperty(
- project_id=project_id,
- user_id=member,
- workspace=workspace,
- created_by=request.user,
- )
- )
-
- ProjectMember.objects.bulk_create(
- project_members, batch_size=10, ignore_conflicts=True
- )
-
- _ = IssueUserProperty.objects.bulk_create(
- issue_props, batch_size=10, ignore_conflicts=True
- )
-
- serializer = ProjectMemberSerializer(project_members, many=True)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
class ProjectMemberUserEndpoint(BaseAPIView):
diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py
index c71df21ac4d..c41441a8566 100644
--- a/apiserver/plane/app/views/workspace/member.py
+++ b/apiserver/plane/app/views/workspace/member.py
@@ -24,7 +24,6 @@
# Module imports
from plane.app.serializers import (
ProjectMemberRoleSerializer,
- TeamSerializer,
UserLiteSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
@@ -34,7 +33,6 @@
from plane.db.models import (
Project,
ProjectMember,
- Team,
User,
Workspace,
WorkspaceMember,
@@ -351,63 +349,4 @@ def get(self, request, slug):
project_members_dict[str(project_id)] = []
project_members_dict[str(project_id)].append(project_member)
- return Response(project_members_dict, status=status.HTTP_200_OK)
-
-
-class TeamMemberViewSet(BaseViewSet):
- serializer_class = TeamSerializer
- model = Team
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- search_fields = [
- "member__display_name",
- "member__first_name",
- ]
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .select_related("workspace", "workspace__owner")
- .prefetch_related("members")
- )
-
- def create(self, request, slug):
- members = list(
- WorkspaceMember.objects.filter(
- workspace__slug=slug,
- member__id__in=request.data.get("members", []),
- is_active=True,
- )
- .annotate(member_str_id=Cast("member", output_field=CharField()))
- .distinct()
- .values_list("member_str_id", flat=True)
- )
-
- if len(members) != len(request.data.get("members", [])):
- users = list(
- set(request.data.get("members", [])).difference(members)
- )
- users = User.objects.filter(pk__in=users)
-
- serializer = UserLiteSerializer(users, many=True)
- return Response(
- {
- "error": f"{len(users)} of the member(s) are not a part of the workspace",
- "members": serializer.data,
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- workspace = Workspace.objects.get(slug=slug)
-
- serializer = TeamSerializer(
- data=request.data, context={"workspace": workspace}
- )
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ return Response(project_members_dict, status=status.HTTP_200_OK)
\ No newline at end of file
diff --git a/apiserver/plane/db/migrations/0085_alter_teammember_unique_together_and_more.py b/apiserver/plane/db/migrations/0085_alter_teammember_unique_together_and_more.py
new file mode 100644
index 00000000000..eabe7af4a21
--- /dev/null
+++ b/apiserver/plane/db/migrations/0085_alter_teammember_unique_together_and_more.py
@@ -0,0 +1,74 @@
+# Generated by Django 4.2.15 on 2024-11-08 13:30
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('db', '0084_remove_label_label_unique_name_project_when_deleted_at_null_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name='teammember',
+ unique_together=None,
+ ),
+ migrations.RemoveField(
+ model_name='teammember',
+ name='created_by',
+ ),
+ migrations.RemoveField(
+ model_name='teammember',
+ name='member',
+ ),
+ migrations.RemoveField(
+ model_name='teammember',
+ name='team',
+ ),
+ migrations.RemoveField(
+ model_name='teammember',
+ name='updated_by',
+ ),
+ migrations.RemoveField(
+ model_name='teammember',
+ name='workspace',
+ ),
+ migrations.AlterUniqueTogether(
+ name='teampage',
+ unique_together=None,
+ ),
+ migrations.RemoveField(
+ model_name='teampage',
+ name='created_by',
+ ),
+ migrations.RemoveField(
+ model_name='teampage',
+ name='page',
+ ),
+ migrations.RemoveField(
+ model_name='teampage',
+ name='team',
+ ),
+ migrations.RemoveField(
+ model_name='teampage',
+ name='updated_by',
+ ),
+ migrations.RemoveField(
+ model_name='teampage',
+ name='workspace',
+ ),
+ migrations.RemoveField(
+ model_name='page',
+ name='teams',
+ ),
+ migrations.DeleteModel(
+ name='Team',
+ ),
+ migrations.DeleteModel(
+ name='TeamMember',
+ ),
+ migrations.DeleteModel(
+ name='TeamPage',
+ ),
+ ]
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index ff930447ac9..577435599d9 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -77,8 +77,6 @@
from .view import IssueView
from .webhook import Webhook, WebhookLog
from .workspace import (
- Team,
- TeamMember,
Workspace,
WorkspaceBaseModel,
WorkspaceMember,
diff --git a/apiserver/plane/db/models/page.py b/apiserver/plane/db/models/page.py
index 433e74a12dc..d0157414aa1 100644
--- a/apiserver/plane/db/models/page.py
+++ b/apiserver/plane/db/models/page.py
@@ -52,9 +52,6 @@ class Page(BaseModel):
projects = models.ManyToManyField(
"db.Project", related_name="pages", through="db.ProjectPage"
)
- teams = models.ManyToManyField(
- "db.Team", related_name="pages", through="db.TeamPage"
- )
class Meta:
verbose_name = "Page"
@@ -169,33 +166,6 @@ class Meta:
def __str__(self):
return f"{self.project.name} {self.page.name}"
-
-class TeamPage(BaseModel):
- team = models.ForeignKey(
- "db.Team", on_delete=models.CASCADE, related_name="team_pages"
- )
- page = models.ForeignKey(
- "db.Page", on_delete=models.CASCADE, related_name="team_pages"
- )
- workspace = models.ForeignKey(
- "db.Workspace", on_delete=models.CASCADE, related_name="team_pages"
- )
-
- class Meta:
- unique_together = ["team", "page", "deleted_at"]
- constraints = [
- models.UniqueConstraint(
- fields=["team", "page"],
- condition=models.Q(deleted_at__isnull=True),
- name="team_page_unique_team_page_when_deleted_at_null",
- )
- ]
- verbose_name = "Team Page"
- verbose_name_plural = "Team Pages"
- db_table = "team_pages"
- ordering = ("-created_at",)
-
-
class PageVersion(BaseModel):
workspace = models.ForeignKey(
"db.Workspace",
diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py
index 8dd4d44f09d..a8d53812005 100644
--- a/apiserver/plane/db/models/workspace.py
+++ b/apiserver/plane/db/models/workspace.py
@@ -259,71 +259,6 @@ def __str__(self):
return f"{self.workspace.name} {self.email} {self.accepted}"
-class Team(BaseModel):
- name = models.CharField(max_length=255, verbose_name="Team Name")
- description = models.TextField(verbose_name="Team Description", blank=True)
- members = models.ManyToManyField(
- settings.AUTH_USER_MODEL,
- blank=True,
- related_name="members",
- through="TeamMember",
- through_fields=("team", "member"),
- )
- workspace = models.ForeignKey(
- Workspace, on_delete=models.CASCADE, related_name="workspace_team"
- )
- logo_props = models.JSONField(default=dict)
-
- def __str__(self):
- """Return name of the team"""
- return f"{self.name} <{self.workspace.name}>"
-
- class Meta:
- unique_together = ["name", "workspace", "deleted_at"]
- constraints = [
- models.UniqueConstraint(
- fields=["name", "workspace"],
- condition=models.Q(deleted_at__isnull=True),
- name="team_unique_name_workspace_when_deleted_at_null",
- )
- ]
- verbose_name = "Team"
- verbose_name_plural = "Teams"
- db_table = "teams"
- ordering = ("-created_at",)
-
-
-class TeamMember(BaseModel):
- workspace = models.ForeignKey(
- Workspace, on_delete=models.CASCADE, related_name="team_member"
- )
- team = models.ForeignKey(
- Team, on_delete=models.CASCADE, related_name="team_member"
- )
- member = models.ForeignKey(
- settings.AUTH_USER_MODEL,
- on_delete=models.CASCADE,
- related_name="team_member",
- )
-
- def __str__(self):
- return self.team.name
-
- class Meta:
- unique_together = ["team", "member", "deleted_at"]
- constraints = [
- models.UniqueConstraint(
- fields=["team", "member"],
- condition=models.Q(deleted_at__isnull=True),
- name="team_member_unique_team_member_when_deleted_at_null",
- )
- ]
- verbose_name = "Team Member"
- verbose_name_plural = "Team Members"
- db_table = "team_members"
- ordering = ("-created_at",)
-
-
class WorkspaceTheme(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="themes"
From 66673279d109a81f64b6c0f7c8318a1853599e2a Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Fri, 8 Nov 2024 19:54:40 +0530
Subject: [PATCH 13/21] chore: migration fixes
---
...e.py => 0086_alter_teammember_unique_together_and_more.py} | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
rename apiserver/plane/db/migrations/{0085_alter_teammember_unique_together_and_more.py => 0086_alter_teammember_unique_together_and_more.py} (92%)
diff --git a/apiserver/plane/db/migrations/0085_alter_teammember_unique_together_and_more.py b/apiserver/plane/db/migrations/0086_alter_teammember_unique_together_and_more.py
similarity index 92%
rename from apiserver/plane/db/migrations/0085_alter_teammember_unique_together_and_more.py
rename to apiserver/plane/db/migrations/0086_alter_teammember_unique_together_and_more.py
index eabe7af4a21..c7cb3c888db 100644
--- a/apiserver/plane/db/migrations/0085_alter_teammember_unique_together_and_more.py
+++ b/apiserver/plane/db/migrations/0086_alter_teammember_unique_together_and_more.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.15 on 2024-11-08 13:30
+# Generated by Django 4.2.15 on 2024-11-08 14:24
from django.db import migrations
@@ -6,7 +6,7 @@
class Migration(migrations.Migration):
dependencies = [
- ('db', '0084_remove_label_label_unique_name_project_when_deleted_at_null_and_more'),
+ ('db', '0085_intake_intakeissue_remove_inboxissue_created_by_and_more'),
]
operations = [
From 6b437ee8c55ed5113e7cd0bad1f5314712a616c2 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Sat, 9 Nov 2024 13:20:10 +0530
Subject: [PATCH 14/21] fix: inbox issue description
---
.../components/inbox/content/issue-root.tsx | 61 +++++++++----------
.../issues/issue-detail/main-content.tsx | 53 ++++++++--------
web/core/hooks/use-issue-description.ts | 9 ---
web/core/store/inbox/inbox-issue.store.ts | 24 ++++++++
4 files changed, 81 insertions(+), 66 deletions(-)
diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx
index a493a836cea..5a04ba3a154 100644
--- a/web/core/components/inbox/content/issue-root.tsx
+++ b/web/core/components/inbox/content/issue-root.tsx
@@ -77,11 +77,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
const issueOperations: TIssueOperations = useMemo(
() => ({
- // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars, arrow-body-style
- fetch: async (_workspaceSlug: string, _projectId: string, _issueId: string) => {
- return;
- },
- // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars, arrow-body-style
+ fetch: async () => {},
remove: async (_workspaceSlug: string, _projectId: string, _issueId: string) => {
try {
await removeIssue(workspaceSlug, projectId, _issueId);
@@ -121,7 +117,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
},
path: pathname,
});
- } catch (error) {
+ } catch {
setToast({
title: "Issue update failed",
type: TOAST_TYPE.ERROR,
@@ -156,7 +152,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
}
},
}),
- [captureIssueEvent, inboxIssue, pathname]
+ [archiveIssue, captureIssueEvent, inboxIssue, pathname, projectId, removeIssue, workspaceSlug]
);
if (!issue?.project_id || !issue?.id) return <>>;
@@ -186,31 +182,32 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
containerClassName="-ml-3"
/>
- "}
- disabled={!isEditable}
- fetchDescription={async () => {
- if (!workspaceSlug || !projectId || !issue.id) {
- throw new Error("Required fields missing while fetching binary description");
- }
- return await inboxIssueService.fetchDescriptionBinary(workspaceSlug, projectId, issue.id);
- }}
- updateDescription={async (data) => {
- if (!workspaceSlug || !projectId || !issue.id) {
- throw new Error("Required fields missing while updating binary description");
- }
- return await inboxIssueService.updateDescriptionBinary(workspaceSlug, projectId, issue.id, {
- description_binary: data,
- });
- }}
- issueId={issue.id}
- issueOperations={issueOperations}
- projectId={issue.project_id}
- setIsSubmitting={(value) => setIsSubmitting(value)}
- workspaceSlug={workspaceSlug}
- />
+ {issue.description_binary !== undefined && (
+ "}
+ disabled={!isEditable}
+ fetchDescription={async () => {
+ if (!workspaceSlug || !projectId || !issue.id) {
+ throw new Error("Required fields missing while fetching binary description");
+ }
+ return await inboxIssueService.fetchDescriptionBinary(workspaceSlug, projectId, issue.id);
+ }}
+ updateDescription={async (data) => {
+ if (!workspaceSlug || !projectId || !issue.id) {
+ throw new Error("Required fields missing while updating binary description");
+ }
+ return await inboxIssue.updateIssueDescription(data);
+ }}
+ issueId={issue.id}
+ issueOperations={issueOperations}
+ projectId={issue.project_id}
+ setIsSubmitting={(value) => setIsSubmitting(value)}
+ workspaceSlug={workspaceSlug}
+ />
+ )}
{currentUser && (
= observer((props) => {
containerClassName="-ml-3"
/>
- "}
- disabled={!isEditable}
- fetchDescription={async () => {
- if (!workspaceSlug || !projectId || !issueId) {
- throw new Error("Required fields missing while fetching binary description");
- }
- return await issueService.fetchDescriptionBinary(workspaceSlug, projectId, issueId);
- }}
- updateDescription={async (data) => {
- if (!workspaceSlug || !issue.project_id || !issue.id) {
- throw new Error("Required fields missing while updating binary description");
- }
- return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
- description_binary: data,
- });
- }}
- issueId={issue.id}
- issueOperations={issueOperations}
- projectId={issue.project_id}
- setIsSubmitting={(value) => setIsSubmitting(value)}
- workspaceSlug={workspaceSlug}
- />
+ {issue.description_binary !== undefined && (
+ "}
+ disabled={!isEditable}
+ fetchDescription={async () => {
+ if (!workspaceSlug || !projectId || !issueId) {
+ throw new Error("Required fields missing while fetching binary description");
+ }
+ return await issueService.fetchDescriptionBinary(workspaceSlug, projectId, issueId);
+ }}
+ updateDescription={async (data) => {
+ if (!workspaceSlug || !issue.project_id || !issue.id) {
+ throw new Error("Required fields missing while updating binary description");
+ }
+ return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
+ description_binary: data,
+ });
+ }}
+ issueId={issue.id}
+ issueOperations={issueOperations}
+ projectId={issue.project_id}
+ setIsSubmitting={(value) => setIsSubmitting(value)}
+ workspaceSlug={workspaceSlug}
+ />
+ )}
{currentUser && (
{
if (descriptionBinary) return;
if (savedDescriptionBinary) {
const savedDescriptionBuffer = convertBase64StringToBinaryData(savedDescriptionBinary);
- console.log("Saved", savedDescriptionBuffer);
const decodedSavedDescription = savedDescriptionBuffer
? new Uint8Array(savedDescriptionBuffer)
: new Uint8Array();
setDescriptionBinary(decodedSavedDescription);
} else {
- console.log("HTML");
const decodedDescriptionHTML = getBinaryDataFromRichTextEditorHTMLString(descriptionHTML ?? "");
setDescriptionBinary(decodedDescriptionHTML);
}
}, [descriptionBinary, descriptionHTML, savedDescriptionBinary]);
- // useEffect(() => {
- // console.log("Setting to null");
- // setDescriptionBinary(null);
- // }, [id]);
-
- console.log("descriptionBinary", descriptionBinary);
-
return {
descriptionBinary,
resolveConflictsAndUpdateDescription,
diff --git a/web/core/store/inbox/inbox-issue.store.ts b/web/core/store/inbox/inbox-issue.store.ts
index e080225aaf2..84d4e1773f5 100644
--- a/web/core/store/inbox/inbox-issue.store.ts
+++ b/web/core/store/inbox/inbox-issue.store.ts
@@ -26,6 +26,7 @@ export interface IInboxIssueStore {
updateInboxIssueDuplicateTo: (issueId: string) => Promise; // connecting the inbox issue to the project existing issue
updateInboxIssueSnoozeTill: (date: Date | undefined) => Promise; // snooze the issue
updateIssue: (issue: Partial) => Promise; // updating the issue
+ updateIssueDescription: (descriptionBinary: string) => Promise; // updating the local issue description
updateProjectIssue: (issue: Partial) => Promise; // updating the issue
fetchIssueActivity: () => Promise; // fetching the issue activity
}
@@ -78,6 +79,7 @@ export class InboxIssueStore implements IInboxIssueStore {
updateInboxIssueDuplicateTo: action,
updateInboxIssueSnoozeTill: action,
updateIssue: action,
+ updateIssueDescription: action,
updateProjectIssue: action,
fetchIssueActivity: action,
});
@@ -175,6 +177,28 @@ export class InboxIssueStore implements IInboxIssueStore {
}
};
+ updateIssueDescription = async (descriptionBinary: string): Promise => {
+ const inboxIssue = clone(this.issue);
+ try {
+ if (!this.issue.id) throw new Error("Issue id is missing");
+ set(this.issue, "description_binary", descriptionBinary);
+ const res = await this.inboxIssueService.updateDescriptionBinary(
+ this.workspaceSlug,
+ this.projectId,
+ this.issue.id,
+ {
+ description_binary: descriptionBinary,
+ }
+ );
+ // fetching activity
+ this.fetchIssueActivity();
+ return res;
+ } catch {
+ set(this.issue, "description_binary", inboxIssue.description_binary);
+ throw new Error("Failed to update local issue description");
+ }
+ };
+
updateProjectIssue = async (issue: Partial) => {
const inboxIssue = clone(this.issue);
try {
From a95143ce707993645b837e8f4549262c180466ac Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Sat, 9 Nov 2024 18:03:24 +0530
Subject: [PATCH 15/21] chore: move update operations to the issue store
---
live/src/core/resolve-conflicts.ts | 49 +++++++++++++++++++
live/src/server.ts | 35 ++-----------
.../use-collaborative-rich-text-editor.ts | 2 +-
.../components/inbox/content/issue-root.tsx | 18 ++++---
.../components/issues/description-input.tsx | 2 -
.../issues/issue-detail/main-content.tsx | 5 +-
.../components/issues/issue-detail/root.tsx | 15 ++++++
.../issues/peek-overview/issue-detail.tsx | 5 +-
.../components/issues/peek-overview/root.tsx | 8 +++
web/core/store/inbox/inbox-issue.store.ts | 4 +-
.../store/issue/issue-details/issue.store.ts | 27 +++++++++-
.../store/issue/issue-details/root.store.ts | 8 ++-
12 files changed, 124 insertions(+), 54 deletions(-)
create mode 100644 live/src/core/resolve-conflicts.ts
diff --git a/live/src/core/resolve-conflicts.ts b/live/src/core/resolve-conflicts.ts
new file mode 100644
index 00000000000..ffaab707c1d
--- /dev/null
+++ b/live/src/core/resolve-conflicts.ts
@@ -0,0 +1,49 @@
+// 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");
+ }
+};
diff --git a/live/src/server.ts b/live/src/server.ts
index 195c8673b33..43d926abd4a 100644
--- a/live/src/server.ts
+++ b/live/src/server.ts
@@ -6,17 +6,12 @@ import * as Sentry from "@sentry/node";
import compression from "compression";
import helmet from "helmet";
import cors from "cors";
-// plane editor
-import {
- applyUpdates,
- convertBase64StringToBinaryData,
- getAllDocumentFormatsFromRichTextEditorBinaryData,
-} from "@plane/editor/lib";
// core hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// helpers
import { errorHandler } from "@/core/helpers/error-handler.js";
import { logger, manualLogger } from "@/core/helpers/logger.js";
+import { resolveDocumentConflicts, TResolveConflictsRequestBody } from "@/core/resolve-conflicts.js";
const app = express();
expressWs(app);
@@ -65,7 +60,7 @@ router.ws("/collaboration", (ws, req) => {
});
app.post("/resolve-document-conflicts", (req, res) => {
- const { original_document, updates } = req.body;
+ const { original_document, updates } = req.body as TResolveConflictsRequestBody;
try {
if (original_document === undefined || updates === undefined) {
res.status(400).send({
@@ -73,30 +68,8 @@ app.post("/resolve-document-conflicts", (req, res) => {
});
throw new Error("Missing required fields");
}
- // 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);
-
- res.status(200).json({
- description_html: contentHTML,
- description_binary: contentBinaryEncoded,
- description: contentJSON,
- });
+ const resolvedDocument = resolveDocumentConflicts(req.body);
+ res.status(200).json(resolvedDocument);
} catch (error) {
console.error("error", error);
res.status(500).send({
diff --git a/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
index 3bffc5b2983..e9a5106d44c 100644
--- a/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
+++ b/packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
@@ -40,11 +40,11 @@ export const useCollaborativeRichTextEditor = (props: TCollaborativeRichTextEdit
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);
}
}
- provider.hasSynced = true;
}, [value, provider.document]);
const editor = useEditor({
diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx
index 5a04ba3a154..f0f739758e5 100644
--- a/web/core/components/inbox/content/issue-root.tsx
+++ b/web/core/components/inbox/content/issue-root.tsx
@@ -62,7 +62,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
}
}, [isSubmitting, setShowAlert, setIsSubmitting]);
- // dervied values
+ // derived values
const issue = inboxIssue.issue;
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
@@ -78,7 +78,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
const issueOperations: TIssueOperations = useMemo(
() => ({
fetch: async () => {},
- remove: async (_workspaceSlug: string, _projectId: string, _issueId: string) => {
+ remove: async (_workspaceSlug, _projectId, _issueId) => {
try {
await removeIssue(workspaceSlug, projectId, _issueId);
setToast({
@@ -105,7 +105,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
});
}
},
- update: async (_workspaceSlug: string, _projectId: string, _issueId: string, data: Partial) => {
+ update: async (_workspaceSlug, _projectId, _issueId, data) => {
try {
await inboxIssue.updateIssue(data);
captureIssueEvent({
@@ -134,7 +134,14 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
});
}
},
- archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
+ updateDescription: async (_workspaceSlug, _projectId, _issueId, descriptionBinary) => {
+ try {
+ return await inboxIssue.updateIssueDescription(descriptionBinary);
+ } catch {
+ throw new Error("Failed to update issue description");
+ }
+ },
+ archive: async (workspaceSlug, projectId, issueId) => {
try {
await archiveIssue(workspaceSlug, projectId, issueId);
captureIssueEvent({
@@ -199,10 +206,9 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
if (!workspaceSlug || !projectId || !issue.id) {
throw new Error("Required fields missing while updating binary description");
}
- return await inboxIssue.updateIssueDescription(data);
+ return await issueOperations.updateDescription(workspaceSlug, projectId, issue.id, data);
}}
issueId={issue.id}
- issueOperations={issueOperations}
projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
workspaceSlug={workspaceSlug}
diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx
index e5215eeeba6..bfea28e1cc5 100644
--- a/web/core/components/issues/description-input.tsx
+++ b/web/core/components/issues/description-input.tsx
@@ -11,7 +11,6 @@ import { EFileAssetType } from "@plane/types/src/enums";
import { Loader } from "@plane/ui";
// components
import { CollaborativeRichTextEditor, CollaborativeRichTextReadOnlyEditor } from "@/components/editor";
-import { TIssueOperations } from "@/components/issues/issue-detail";
// helpers
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
// hooks
@@ -28,7 +27,6 @@ export type IssueDescriptionInputProps = {
disabled?: boolean;
fetchDescription: () => Promise;
issueId: string;
- issueOperations: TIssueOperations;
key: string;
placeholder?: string | ((isFocused: boolean, value: string) => string);
projectId: string;
diff --git a/web/core/components/issues/issue-detail/main-content.tsx b/web/core/components/issues/issue-detail/main-content.tsx
index fa6a62590fa..81393cc93a8 100644
--- a/web/core/components/issues/issue-detail/main-content.tsx
+++ b/web/core/components/issues/issue-detail/main-content.tsx
@@ -134,12 +134,9 @@ export const IssueMainContent: React.FC = observer((props) => {
if (!workspaceSlug || !issue.project_id || !issue.id) {
throw new Error("Required fields missing while updating binary description");
}
- return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
- description_binary: data,
- });
+ return await issueOperations.updateDescription(workspaceSlug, issue.project_id, issue.id, data);
}}
issueId={issue.id}
- issueOperations={issueOperations}
projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
workspaceSlug={workspaceSlug}
diff --git a/web/core/components/issues/issue-detail/root.tsx b/web/core/components/issues/issue-detail/root.tsx
index 9db4b1ab9e9..572312423d1 100644
--- a/web/core/components/issues/issue-detail/root.tsx
+++ b/web/core/components/issues/issue-detail/root.tsx
@@ -26,6 +26,12 @@ import { IssueDetailsSidebar } from "./sidebar";
export type TIssueOperations = {
fetch: (workspaceSlug: string, projectId: string, issueId: string, loader?: boolean) => Promise;
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise;
+ updateDescription: (
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string,
+ descriptionBinary: string
+ ) => Promise;
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise;
archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise;
restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise;
@@ -64,6 +70,7 @@ export const IssueDetailRoot: FC = observer((props) => {
issue: { getIssueById },
fetchIssue,
updateIssue,
+ updateIssueDescription,
removeIssue,
archiveIssue,
addCycleToIssue,
@@ -118,6 +125,13 @@ export const IssueDetailRoot: FC = 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) => {
try {
if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId);
@@ -317,6 +331,7 @@ export const IssueDetailRoot: FC = observer((props) => {
is_archived,
fetchIssue,
updateIssue,
+ updateIssueDescription,
removeIssue,
archiveIssue,
removeArchivedIssue,
diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx
index bf8321abfdd..5c742762c84 100644
--- a/web/core/components/issues/peek-overview/issue-detail.tsx
+++ b/web/core/components/issues/peek-overview/issue-detail.tsx
@@ -119,12 +119,9 @@ export const PeekOverviewIssueDetails: FC = observer(
if (!workspaceSlug || !issue.project_id || !issue.id) {
throw new Error("Required fields missing while updating binary description");
}
- return await issueService.updateDescriptionBinary(workspaceSlug, issue.project_id, issue.id, {
- description_binary: data,
- });
+ return await issueOperations.updateDescription(workspaceSlug, issue.project_id, issue.id, data);
}}
issueId={issue.id}
- issueOperations={issueOperations}
projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
workspaceSlug={workspaceSlug}
diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx
index 70eb51d81b8..94a59b44592 100644
--- a/web/core/components/issues/peek-overview/root.tsx
+++ b/web/core/components/issues/peek-overview/root.tsx
@@ -39,6 +39,7 @@ export const IssuePeekOverview: FC = observer((props) => {
setPeekIssue,
issue: { fetchIssue, getIsFetchingIssueDetails },
fetchActivities,
+ updateIssueDescription,
} = useIssueDetail();
const { issues } = useIssuesStore();
@@ -92,6 +93,13 @@ export const IssuePeekOverview: FC = 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) => {
try {
return issues?.removeIssue(workspaceSlug, projectId, issueId).then(() => {
diff --git a/web/core/store/inbox/inbox-issue.store.ts b/web/core/store/inbox/inbox-issue.store.ts
index 84d4e1773f5..30a03207d8d 100644
--- a/web/core/store/inbox/inbox-issue.store.ts
+++ b/web/core/store/inbox/inbox-issue.store.ts
@@ -178,10 +178,8 @@ export class InboxIssueStore implements IInboxIssueStore {
};
updateIssueDescription = async (descriptionBinary: string): Promise => {
- const inboxIssue = clone(this.issue);
try {
if (!this.issue.id) throw new Error("Issue id is missing");
- set(this.issue, "description_binary", descriptionBinary);
const res = await this.inboxIssueService.updateDescriptionBinary(
this.workspaceSlug,
this.projectId,
@@ -190,11 +188,11 @@ export class InboxIssueStore implements IInboxIssueStore {
description_binary: descriptionBinary,
}
);
+ set(this.issue, "description_binary", descriptionBinary);
// fetching activity
this.fetchIssueActivity();
return res;
} catch {
- set(this.issue, "description_binary", inboxIssue.description_binary);
throw new Error("Failed to update local issue description");
}
};
diff --git a/web/core/store/issue/issue-details/issue.store.ts b/web/core/store/issue/issue-details/issue.store.ts
index 6d1d653d27d..89a9c84d556 100644
--- a/web/core/store/issue/issue-details/issue.store.ts
+++ b/web/core/store/issue/issue-details/issue.store.ts
@@ -7,6 +7,7 @@ import { persistence } from "@/local-db/storage.sqlite";
// services
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
// types
+import { IIssueRootStore } from "../root.store";
import { IIssueDetail } from "./root.store";
export interface IIssueStoreActions {
@@ -18,6 +19,12 @@ export interface IIssueStoreActions {
issueStatus?: "DEFAULT" | "DRAFT"
) => Promise;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise;
+ updateIssueDescription: (
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string,
+ descriptionBinary: string
+ ) => Promise;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise;
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise;
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise;
@@ -44,19 +51,21 @@ export class IssueStore implements IIssueStore {
fetchingIssueDetails: string | undefined = undefined;
localDBIssueDescription: string | undefined = undefined;
// root store
+ rootIssueStore: IIssueRootStore;
rootIssueDetailStore: IIssueDetail;
// services
issueService;
issueArchiveService;
issueDraftService;
- constructor(rootStore: IIssueDetail) {
+ constructor(rootStore: IIssueRootStore) {
makeObservable(this, {
fetchingIssueDetails: observable.ref,
localDBIssueDescription: observable.ref,
});
// root store
- this.rootIssueDetailStore = rootStore;
+ this.rootIssueStore = rootStore;
+ this.rootIssueDetailStore = rootStore.issueDetail;
// services
this.issueService = new IssueService();
this.issueArchiveService = new IssueArchiveService();
@@ -195,6 +204,20 @@ export class IssueStore implements IIssueStore {
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
};
+ updateIssueDescription = async (
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string,
+ descriptionBinary: string
+ ): Promise => {
+ 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) =>
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
diff --git a/web/core/store/issue/issue-details/root.store.ts b/web/core/store/issue/issue-details/root.store.ts
index e6e0ca8d0b2..b4370903394 100644
--- a/web/core/store/issue/issue-details/root.store.ts
+++ b/web/core/store/issue/issue-details/root.store.ts
@@ -192,7 +192,7 @@ export class IssueDetail implements IIssueDetail {
// store
this.rootIssueStore = rootStore;
- this.issue = new IssueStore(this);
+ this.issue = new IssueStore(rootStore);
this.reaction = new IssueReactionStore(this);
this.attachment = new IssueAttachmentStore(rootStore);
this.activity = new IssueActivityStore(rootStore.rootStore as RootStore);
@@ -257,6 +257,12 @@ export class IssueDetail implements IIssueDetail {
) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueStatus);
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) =>
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) =>
this.issue.removeIssue(workspaceSlug, projectId, issueId);
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
From e3ac9efe130f30504142b6717c47d53bf2f69931 Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Tue, 12 Nov 2024 14:26:12 +0530
Subject: [PATCH 16/21] fix: merge conflicts
---
apiserver/plane/app/urls/intake.py | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/apiserver/plane/app/urls/intake.py b/apiserver/plane/app/urls/intake.py
index be2f3a053f4..d4f160577e5 100644
--- a/apiserver/plane/app/urls/intake.py
+++ b/apiserver/plane/app/urls/intake.py
@@ -92,4 +92,14 @@
),
name="inbox-issue",
),
+ path(
+ "workspaces//projects//inbox-issues//description/",
+ IntakeIssueViewSet.as_view(
+ {
+ "get": "retrieve_description",
+ "post": "update_description",
+ }
+ ),
+ name="inbox-issue-description",
+ ),
]
From e6787d82e758081d4c6cc10e83ce9ad12c5ab3d2 Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Tue, 12 Nov 2024 14:45:36 +0530
Subject: [PATCH 17/21] chore: reverted the commit
---
apiserver/plane/app/serializers/__init__.py | 1 +
apiserver/plane/app/serializers/workspace.py | 54 ++++++++++++++
apiserver/plane/app/urls/project.py | 6 ++
apiserver/plane/app/urls/workspace.py | 23 ++++++
apiserver/plane/app/views/__init__.py | 2 +
apiserver/plane/app/views/project/member.py | 49 ++++++++++++
apiserver/plane/app/views/workspace/member.py | 63 +++++++++++++++-
...ter_teammember_unique_together_and_more.py | 74 -------------------
apiserver/plane/db/models/__init__.py | 2 +
apiserver/plane/db/models/page.py | 30 ++++++++
apiserver/plane/db/models/workspace.py | 65 ++++++++++++++++
11 files changed, 294 insertions(+), 75 deletions(-)
delete mode 100644 apiserver/plane/db/migrations/0086_alter_teammember_unique_together_and_more.py
diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py
index 6c9e1fed17a..2e2c91baa1f 100644
--- a/apiserver/plane/app/serializers/__init__.py
+++ b/apiserver/plane/app/serializers/__init__.py
@@ -13,6 +13,7 @@
from .workspace import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
+ TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py
index ce1c75dbf53..4f106022606 100644
--- a/apiserver/plane/app/serializers/workspace.py
+++ b/apiserver/plane/app/serializers/workspace.py
@@ -9,6 +9,8 @@
User,
Workspace,
WorkspaceMember,
+ Team,
+ TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
@@ -64,6 +66,7 @@ class Meta:
class WorkspaceMemberMeSerializer(BaseSerializer):
draft_issue_count = serializers.IntegerField(read_only=True)
+
class Meta:
model = WorkspaceMember
fields = "__all__"
@@ -97,6 +100,57 @@ class Meta:
"updated_at",
]
+
+class TeamSerializer(BaseSerializer):
+ members_detail = UserLiteSerializer(
+ read_only=True, source="members", many=True
+ )
+ members = serializers.ListField(
+ child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
+ write_only=True,
+ required=False,
+ )
+
+ class Meta:
+ model = Team
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+ def create(self, validated_data, **kwargs):
+ if "members" in validated_data:
+ members = validated_data.pop("members")
+ workspace = self.context["workspace"]
+ team = Team.objects.create(**validated_data, workspace=workspace)
+ team_members = [
+ TeamMember(member=member, team=team, workspace=workspace)
+ for member in members
+ ]
+ TeamMember.objects.bulk_create(team_members, batch_size=10)
+ return team
+ team = Team.objects.create(**validated_data)
+ return team
+
+ def update(self, instance, validated_data):
+ if "members" in validated_data:
+ members = validated_data.pop("members")
+ TeamMember.objects.filter(team=instance).delete()
+ team_members = [
+ TeamMember(
+ member=member, team=instance, workspace=instance.workspace
+ )
+ for member in members
+ ]
+ TeamMember.objects.bulk_create(team_members, batch_size=10)
+ return super().update(instance, validated_data)
+ return super().update(instance, validated_data)
+
+
class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
diff --git a/apiserver/plane/app/urls/project.py b/apiserver/plane/app/urls/project.py
index 4ea4522494d..0807c7616a2 100644
--- a/apiserver/plane/app/urls/project.py
+++ b/apiserver/plane/app/urls/project.py
@@ -7,6 +7,7 @@
ProjectMemberViewSet,
ProjectMemberUserEndpoint,
ProjectJoinEndpoint,
+ AddTeamToProjectEndpoint,
ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
@@ -115,6 +116,11 @@
),
name="project-member",
),
+ path(
+ "workspaces//projects//team-invite/",
+ AddTeamToProjectEndpoint.as_view(),
+ name="projects",
+ ),
path(
"workspaces//projects//project-views/",
ProjectUserViewsEndpoint.as_view(),
diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py
index ff5708e4a8a..6481f5691cd 100644
--- a/apiserver/plane/app/urls/workspace.py
+++ b/apiserver/plane/app/urls/workspace.py
@@ -10,6 +10,7 @@
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
WorkSpaceAvailabilityCheckEndpoint,
+ TeamMemberViewSet,
UserLastProjectWithWorkspaceEndpoint,
WorkspaceThemeViewSet,
WorkspaceUserProfileStatsEndpoint,
@@ -126,6 +127,28 @@
),
name="leave-workspace-members",
),
+ path(
+ "workspaces//teams/",
+ TeamMemberViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="workspace-team-members",
+ ),
+ path(
+ "workspaces//teams//",
+ TeamMemberViewSet.as_view(
+ {
+ "put": "update",
+ "patch": "partial_update",
+ "delete": "destroy",
+ "get": "retrieve",
+ }
+ ),
+ name="workspace-team-members",
+ ),
path(
"users/last-visited-workspace/",
UserLastProjectWithWorkspaceEndpoint.as_view(),
diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py
index e67b9d2efb7..159686fa3ac 100644
--- a/apiserver/plane/app/views/__init__.py
+++ b/apiserver/plane/app/views/__init__.py
@@ -16,6 +16,7 @@
from .project.member import (
ProjectMemberViewSet,
+ AddTeamToProjectEndpoint,
ProjectMemberUserEndpoint,
UserProjectRolesEndpoint,
)
@@ -48,6 +49,7 @@
from .workspace.member import (
WorkSpaceMemberViewSet,
+ TeamMemberViewSet,
WorkspaceMemberUserEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceMemberUserViewsEndpoint,
diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py
index 36b2d3076c2..ccb5e75216e 100644
--- a/apiserver/plane/app/views/project/member.py
+++ b/apiserver/plane/app/views/project/member.py
@@ -21,6 +21,7 @@
Project,
ProjectMember,
Workspace,
+ TeamMember,
IssueUserProperty,
WorkspaceMember,
)
@@ -341,6 +342,54 @@ def leave(self, request, slug, project_id):
return Response(status=status.HTTP_204_NO_CONTENT)
+class AddTeamToProjectEndpoint(BaseAPIView):
+ permission_classes = [
+ ProjectBasePermission,
+ ]
+
+ def post(self, request, slug, project_id):
+ team_members = TeamMember.objects.filter(
+ workspace__slug=slug, team__in=request.data.get("teams", [])
+ ).values_list("member", flat=True)
+
+ if len(team_members) == 0:
+ return Response(
+ {"error": "No such team exists"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ workspace = Workspace.objects.get(slug=slug)
+
+ project_members = []
+ issue_props = []
+ for member in team_members:
+ project_members.append(
+ ProjectMember(
+ project_id=project_id,
+ member_id=member,
+ workspace=workspace,
+ created_by=request.user,
+ )
+ )
+ issue_props.append(
+ IssueUserProperty(
+ project_id=project_id,
+ user_id=member,
+ workspace=workspace,
+ created_by=request.user,
+ )
+ )
+
+ ProjectMember.objects.bulk_create(
+ project_members, batch_size=10, ignore_conflicts=True
+ )
+
+ _ = IssueUserProperty.objects.bulk_create(
+ issue_props, batch_size=10, ignore_conflicts=True
+ )
+
+ serializer = ProjectMemberSerializer(project_members, many=True)
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
class ProjectMemberUserEndpoint(BaseAPIView):
diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py
index c41441a8566..c71df21ac4d 100644
--- a/apiserver/plane/app/views/workspace/member.py
+++ b/apiserver/plane/app/views/workspace/member.py
@@ -24,6 +24,7 @@
# Module imports
from plane.app.serializers import (
ProjectMemberRoleSerializer,
+ TeamSerializer,
UserLiteSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
@@ -33,6 +34,7 @@
from plane.db.models import (
Project,
ProjectMember,
+ Team,
User,
Workspace,
WorkspaceMember,
@@ -349,4 +351,63 @@ def get(self, request, slug):
project_members_dict[str(project_id)] = []
project_members_dict[str(project_id)].append(project_member)
- return Response(project_members_dict, status=status.HTTP_200_OK)
\ No newline at end of file
+ return Response(project_members_dict, status=status.HTTP_200_OK)
+
+
+class TeamMemberViewSet(BaseViewSet):
+ serializer_class = TeamSerializer
+ model = Team
+ permission_classes = [
+ WorkSpaceAdminPermission,
+ ]
+
+ search_fields = [
+ "member__display_name",
+ "member__first_name",
+ ]
+
+ def get_queryset(self):
+ return self.filter_queryset(
+ super()
+ .get_queryset()
+ .filter(workspace__slug=self.kwargs.get("slug"))
+ .select_related("workspace", "workspace__owner")
+ .prefetch_related("members")
+ )
+
+ def create(self, request, slug):
+ members = list(
+ WorkspaceMember.objects.filter(
+ workspace__slug=slug,
+ member__id__in=request.data.get("members", []),
+ is_active=True,
+ )
+ .annotate(member_str_id=Cast("member", output_field=CharField()))
+ .distinct()
+ .values_list("member_str_id", flat=True)
+ )
+
+ if len(members) != len(request.data.get("members", [])):
+ users = list(
+ set(request.data.get("members", [])).difference(members)
+ )
+ users = User.objects.filter(pk__in=users)
+
+ serializer = UserLiteSerializer(users, many=True)
+ return Response(
+ {
+ "error": f"{len(users)} of the member(s) are not a part of the workspace",
+ "members": serializer.data,
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ workspace = Workspace.objects.get(slug=slug)
+
+ serializer = TeamSerializer(
+ data=request.data, context={"workspace": workspace}
+ )
+ if serializer.is_valid():
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
diff --git a/apiserver/plane/db/migrations/0086_alter_teammember_unique_together_and_more.py b/apiserver/plane/db/migrations/0086_alter_teammember_unique_together_and_more.py
deleted file mode 100644
index c7cb3c888db..00000000000
--- a/apiserver/plane/db/migrations/0086_alter_teammember_unique_together_and_more.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# Generated by Django 4.2.15 on 2024-11-08 14:24
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('db', '0085_intake_intakeissue_remove_inboxissue_created_by_and_more'),
- ]
-
- operations = [
- migrations.AlterUniqueTogether(
- name='teammember',
- unique_together=None,
- ),
- migrations.RemoveField(
- model_name='teammember',
- name='created_by',
- ),
- migrations.RemoveField(
- model_name='teammember',
- name='member',
- ),
- migrations.RemoveField(
- model_name='teammember',
- name='team',
- ),
- migrations.RemoveField(
- model_name='teammember',
- name='updated_by',
- ),
- migrations.RemoveField(
- model_name='teammember',
- name='workspace',
- ),
- migrations.AlterUniqueTogether(
- name='teampage',
- unique_together=None,
- ),
- migrations.RemoveField(
- model_name='teampage',
- name='created_by',
- ),
- migrations.RemoveField(
- model_name='teampage',
- name='page',
- ),
- migrations.RemoveField(
- model_name='teampage',
- name='team',
- ),
- migrations.RemoveField(
- model_name='teampage',
- name='updated_by',
- ),
- migrations.RemoveField(
- model_name='teampage',
- name='workspace',
- ),
- migrations.RemoveField(
- model_name='page',
- name='teams',
- ),
- migrations.DeleteModel(
- name='Team',
- ),
- migrations.DeleteModel(
- name='TeamMember',
- ),
- migrations.DeleteModel(
- name='TeamPage',
- ),
- ]
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index 577435599d9..ff930447ac9 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -77,6 +77,8 @@
from .view import IssueView
from .webhook import Webhook, WebhookLog
from .workspace import (
+ Team,
+ TeamMember,
Workspace,
WorkspaceBaseModel,
WorkspaceMember,
diff --git a/apiserver/plane/db/models/page.py b/apiserver/plane/db/models/page.py
index d0157414aa1..433e74a12dc 100644
--- a/apiserver/plane/db/models/page.py
+++ b/apiserver/plane/db/models/page.py
@@ -52,6 +52,9 @@ class Page(BaseModel):
projects = models.ManyToManyField(
"db.Project", related_name="pages", through="db.ProjectPage"
)
+ teams = models.ManyToManyField(
+ "db.Team", related_name="pages", through="db.TeamPage"
+ )
class Meta:
verbose_name = "Page"
@@ -166,6 +169,33 @@ class Meta:
def __str__(self):
return f"{self.project.name} {self.page.name}"
+
+class TeamPage(BaseModel):
+ team = models.ForeignKey(
+ "db.Team", on_delete=models.CASCADE, related_name="team_pages"
+ )
+ page = models.ForeignKey(
+ "db.Page", on_delete=models.CASCADE, related_name="team_pages"
+ )
+ workspace = models.ForeignKey(
+ "db.Workspace", on_delete=models.CASCADE, related_name="team_pages"
+ )
+
+ class Meta:
+ unique_together = ["team", "page", "deleted_at"]
+ constraints = [
+ models.UniqueConstraint(
+ fields=["team", "page"],
+ condition=models.Q(deleted_at__isnull=True),
+ name="team_page_unique_team_page_when_deleted_at_null",
+ )
+ ]
+ verbose_name = "Team Page"
+ verbose_name_plural = "Team Pages"
+ db_table = "team_pages"
+ ordering = ("-created_at",)
+
+
class PageVersion(BaseModel):
workspace = models.ForeignKey(
"db.Workspace",
diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py
index a8d53812005..8dd4d44f09d 100644
--- a/apiserver/plane/db/models/workspace.py
+++ b/apiserver/plane/db/models/workspace.py
@@ -259,6 +259,71 @@ def __str__(self):
return f"{self.workspace.name} {self.email} {self.accepted}"
+class Team(BaseModel):
+ name = models.CharField(max_length=255, verbose_name="Team Name")
+ description = models.TextField(verbose_name="Team Description", blank=True)
+ members = models.ManyToManyField(
+ settings.AUTH_USER_MODEL,
+ blank=True,
+ related_name="members",
+ through="TeamMember",
+ through_fields=("team", "member"),
+ )
+ workspace = models.ForeignKey(
+ Workspace, on_delete=models.CASCADE, related_name="workspace_team"
+ )
+ logo_props = models.JSONField(default=dict)
+
+ def __str__(self):
+ """Return name of the team"""
+ return f"{self.name} <{self.workspace.name}>"
+
+ class Meta:
+ unique_together = ["name", "workspace", "deleted_at"]
+ constraints = [
+ models.UniqueConstraint(
+ fields=["name", "workspace"],
+ condition=models.Q(deleted_at__isnull=True),
+ name="team_unique_name_workspace_when_deleted_at_null",
+ )
+ ]
+ verbose_name = "Team"
+ verbose_name_plural = "Teams"
+ db_table = "teams"
+ ordering = ("-created_at",)
+
+
+class TeamMember(BaseModel):
+ workspace = models.ForeignKey(
+ Workspace, on_delete=models.CASCADE, related_name="team_member"
+ )
+ team = models.ForeignKey(
+ Team, on_delete=models.CASCADE, related_name="team_member"
+ )
+ member = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE,
+ related_name="team_member",
+ )
+
+ def __str__(self):
+ return self.team.name
+
+ class Meta:
+ unique_together = ["team", "member", "deleted_at"]
+ constraints = [
+ models.UniqueConstraint(
+ fields=["team", "member"],
+ condition=models.Q(deleted_at__isnull=True),
+ name="team_member_unique_team_member_when_deleted_at_null",
+ )
+ ]
+ verbose_name = "Team Member"
+ verbose_name_plural = "Team Members"
+ db_table = "team_members"
+ ordering = ("-created_at",)
+
+
class WorkspaceTheme(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="themes"
From ea4d2010f482a687ff2084f6204851018d9dc0a8 Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Tue, 12 Nov 2024 14:59:04 +0530
Subject: [PATCH 18/21] chore: removed the unwanted imports
---
apiserver/plane/app/views/intake/base.py | 9 ++++++++-
apiserver/plane/app/views/issue/base.py | 8 +++++++-
apiserver/plane/app/views/workspace/draft.py | 8 +++++++-
..._intakeissue_remove_inboxissue_created_by_and_more.py | 2 --
apiserver/plane/space/urls/intake.py | 1 -
5 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/apiserver/plane/app/views/intake/base.py b/apiserver/plane/app/views/intake/base.py
index d052ca12013..394957884c9 100644
--- a/apiserver/plane/app/views/intake/base.py
+++ b/apiserver/plane/app/views/intake/base.py
@@ -670,6 +670,7 @@ def stream_data():
)
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
@@ -685,7 +686,13 @@ def update_description(self, request, slug, project_id, pk):
"updates": request.data.get("description_binary"),
}
base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
- response = requests.post(base_url, json=data, headers=None)
+ 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(
diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py
index c78be9a0c74..09b23f11a47 100644
--- a/apiserver/plane/app/views/issue/base.py
+++ b/apiserver/plane/app/views/issue/base.py
@@ -770,7 +770,13 @@ def update_description(self, request, slug, project_id, pk):
"updates": request.data.get("description_binary"),
}
base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
- response = requests.post(base_url, json=data, headers=None)
+ 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(
diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py
index 679b394e550..1d68256c0ca 100644
--- a/apiserver/plane/app/views/workspace/draft.py
+++ b/apiserver/plane/app/views/workspace/draft.py
@@ -393,7 +393,13 @@ def update_description(self, request, slug, pk):
"updates": request.data.get("description_binary"),
}
base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
- response = requests.post(base_url, data=data, headers=None)
+ 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(
diff --git a/apiserver/plane/db/migrations/0085_intake_intakeissue_remove_inboxissue_created_by_and_more.py b/apiserver/plane/db/migrations/0085_intake_intakeissue_remove_inboxissue_created_by_and_more.py
index 36cf73bc541..16c4167cf66 100644
--- a/apiserver/plane/db/migrations/0085_intake_intakeissue_remove_inboxissue_created_by_and_more.py
+++ b/apiserver/plane/db/migrations/0085_intake_intakeissue_remove_inboxissue_created_by_and_more.py
@@ -1,9 +1,7 @@
# Generated by Django 4.2.15 on 2024-11-06 08:41
-from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
-import uuid
class Migration(migrations.Migration):
diff --git a/apiserver/plane/space/urls/intake.py b/apiserver/plane/space/urls/intake.py
index 9f43a28098b..350157c950d 100644
--- a/apiserver/plane/space/urls/intake.py
+++ b/apiserver/plane/space/urls/intake.py
@@ -3,7 +3,6 @@
from plane.space.views import (
IntakeIssuePublicViewSet,
- IssueVotePublicViewSet,
WorkspaceProjectDeployBoardEndpoint,
)
From 4d7d4b60441309d0c23e1edc7b80013fb52f8439 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Tue, 12 Nov 2024 15:32:36 +0530
Subject: [PATCH 19/21] chore: remove unnecessary props
---
.../collaborative-read-only-editor.tsx | 6 +++---
.../components/inbox/content/issue-root.tsx | 21 ++++---------------
.../components/issues/description-input.tsx | 5 +----
.../issues/issue-detail/main-content.tsx | 18 +++-------------
.../issues/peek-overview/issue-detail.tsx | 18 +++-------------
.../components/issues/peek-overview/root.tsx | 15 ++++++++++++-
web/core/hooks/use-issue-description.ts | 3 +--
7 files changed, 29 insertions(+), 57 deletions(-)
diff --git a/web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx b/web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx
index 253acc75e80..2e80f86c75e 100644
--- a/web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx
+++ b/web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx
@@ -18,8 +18,8 @@ type RichTextReadOnlyEditorWrapperProps = Omit<
ICollaborativeRichTextReadOnlyEditor,
"fileHandler" | "mentionHandler" | "value"
> & {
+ descriptionBinary: string | null;
descriptionHTML: string;
- fetchDescription: () => Promise;
projectId?: string;
workspaceSlug: string;
};
@@ -27,12 +27,12 @@ type RichTextReadOnlyEditorWrapperProps = Omit<
export const CollaborativeRichTextReadOnlyEditor = React.forwardRef<
EditorReadOnlyRefApi,
RichTextReadOnlyEditorWrapperProps
->(({ descriptionHTML, fetchDescription, projectId, workspaceSlug, ...props }, ref) => {
+>(({ descriptionBinary: savedDescriptionBinary, descriptionHTML, projectId, workspaceSlug, ...props }, ref) => {
const { mentionHighlights } = useMention({});
const { descriptionBinary } = useIssueDescription({
+ descriptionBinary: savedDescriptionBinary,
descriptionHTML,
- fetchDescription,
});
if (!descriptionBinary)
diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx
index f0f739758e5..f547a552b87 100644
--- a/web/core/components/inbox/content/issue-root.tsx
+++ b/web/core/components/inbox/content/issue-root.tsx
@@ -3,8 +3,6 @@
import { Dispatch, SetStateAction, useEffect, useMemo } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
-// plane types
-import { TIssue } from "@plane/types";
// plane ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
@@ -27,9 +25,7 @@ import useReloadConfirmations from "@/hooks/use-reload-confirmation";
// store types
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
-// services
-import { InboxIssueService } from "@/services/inbox";
-const inboxIssueService = new InboxIssueService();
+// store
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
type Props = {
@@ -196,18 +192,9 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
descriptionBinary={issue.description_binary}
descriptionHTML={issue.description_html ?? ""}
disabled={!isEditable}
- fetchDescription={async () => {
- if (!workspaceSlug || !projectId || !issue.id) {
- throw new Error("Required fields missing while fetching binary description");
- }
- return await inboxIssueService.fetchDescriptionBinary(workspaceSlug, projectId, issue.id);
- }}
- updateDescription={async (data) => {
- if (!workspaceSlug || !projectId || !issue.id) {
- throw new Error("Required fields missing while updating binary description");
- }
- return await issueOperations.updateDescription(workspaceSlug, projectId, issue.id, data);
- }}
+ updateDescription={async (data) =>
+ await issueOperations.updateDescription(workspaceSlug, projectId, issue.id ?? "", data)
+ }
issueId={issue.id}
projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx
index bfea28e1cc5..7e6229d9e42 100644
--- a/web/core/components/issues/description-input.tsx
+++ b/web/core/components/issues/description-input.tsx
@@ -25,7 +25,6 @@ export type IssueDescriptionInputProps = {
descriptionBinary: string | null;
descriptionHTML: string;
disabled?: boolean;
- fetchDescription: () => Promise;
issueId: string;
key: string;
placeholder?: string | ((isFocused: boolean, value: string) => string);
@@ -41,7 +40,6 @@ export const IssueDescriptionInput: FC = observer((p
descriptionBinary: savedDescriptionBinary,
descriptionHTML,
disabled,
- fetchDescription,
issueId,
placeholder,
projectId,
@@ -59,7 +57,6 @@ export const IssueDescriptionInput: FC = observer((p
const { descriptionBinary, resolveConflictsAndUpdateDescription } = useIssueDescription({
descriptionBinary: savedDescriptionBinary,
descriptionHTML,
- id: issueId,
updateDescription,
});
@@ -136,8 +133,8 @@ export const IssueDescriptionInput: FC = observer((p
) : (
= observer((props) => {
descriptionBinary={issue.description_binary}
descriptionHTML={issue.description_html ?? ""}
disabled={!isEditable}
- fetchDescription={async () => {
- if (!workspaceSlug || !projectId || !issueId) {
- throw new Error("Required fields missing while fetching binary description");
- }
- return await issueService.fetchDescriptionBinary(workspaceSlug, projectId, issueId);
- }}
- updateDescription={async (data) => {
- if (!workspaceSlug || !issue.project_id || !issue.id) {
- throw new Error("Required fields missing while updating binary description");
- }
- return await issueOperations.updateDescription(workspaceSlug, issue.project_id, issue.id, data);
- }}
+ updateDescription={async (data) =>
+ await issueOperations.updateDescription(workspaceSlug, issue.project_id ?? "", issue.id, data)
+ }
issueId={issue.id}
projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx
index 5c742762c84..6aec30422bb 100644
--- a/web/core/components/issues/peek-overview/issue-detail.tsx
+++ b/web/core/components/issues/peek-overview/issue-detail.tsx
@@ -14,9 +14,6 @@ import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
// plane web hooks
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
-// services
-import { IssueService } from "@/services/issue";
-const issueService = new IssueService();
// local components
import { IssueDescriptionInput } from "../description-input";
import { IssueReaction } from "../issue-detail/reactions";
@@ -109,18 +106,9 @@ export const PeekOverviewIssueDetails: FC = observer(
descriptionBinary={issue.description_binary}
descriptionHTML={issue.description_html ?? ""}
disabled={disabled}
- fetchDescription={async () => {
- if (!workspaceSlug || !issue.project_id || !issue.id) {
- throw new Error("Required fields missing while fetching binary description");
- }
- return await issueService.fetchDescriptionBinary(workspaceSlug, issue.project_id, issue.id);
- }}
- updateDescription={async (data) => {
- if (!workspaceSlug || !issue.project_id || !issue.id) {
- throw new Error("Required fields missing while updating binary description");
- }
- return await issueOperations.updateDescription(workspaceSlug, issue.project_id, issue.id, data);
- }}
+ updateDescription={async (data) =>
+ await issueOperations.updateDescription(workspaceSlug, issue.project_id ?? "", issue.id, data)
+ }
issueId={issue.id}
projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx
index 94a59b44592..f7e1f0cfb5b 100644
--- a/web/core/components/issues/peek-overview/root.tsx
+++ b/web/core/components/issues/peek-overview/root.tsx
@@ -94,6 +94,9 @@ export const IssuePeekOverview: FC = 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 {
@@ -326,7 +329,17 @@ export const IssuePeekOverview: FC = observer((props) => {
}
},
}),
- [fetchIssue, is_draft, issues, fetchActivities, captureIssueEvent, pathname, removeRoutePeekId, restoreIssue]
+ [
+ fetchIssue,
+ is_draft,
+ issues,
+ fetchActivities,
+ captureIssueEvent,
+ pathname,
+ removeRoutePeekId,
+ restoreIssue,
+ updateIssueDescription,
+ ]
);
useEffect(() => {
diff --git a/web/core/hooks/use-issue-description.ts b/web/core/hooks/use-issue-description.ts
index 23c7194e482..5493ffae02d 100644
--- a/web/core/hooks/use-issue-description.ts
+++ b/web/core/hooks/use-issue-description.ts
@@ -9,12 +9,11 @@ import {
type TArgs = {
descriptionBinary: string | null;
descriptionHTML: string | null;
- id: string;
updateDescription?: (data: string) => Promise;
};
export const useIssueDescription = (args: TArgs) => {
- const { descriptionBinary: savedDescriptionBinary, descriptionHTML, id, updateDescription } = args;
+ const { descriptionBinary: savedDescriptionBinary, descriptionHTML, updateDescription } = args;
// states
const [descriptionBinary, setDescriptionBinary] = useState(null);
// update description
From 1fcc3ab70a918d408aef5356da2fe47805ea5e0f Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Tue, 12 Nov 2024 16:13:14 +0530
Subject: [PATCH 20/21] chore: remove unused services
---
web/core/services/inbox/inbox-issue.service.ts | 16 ----------------
web/core/services/issue/issue.service.ts | 13 -------------
.../store/issue/issue-details/issue.store.ts | 4 ++--
web/core/store/issue/issue-details/root.store.ts | 2 +-
4 files changed, 3 insertions(+), 32 deletions(-)
diff --git a/web/core/services/inbox/inbox-issue.service.ts b/web/core/services/inbox/inbox-issue.service.ts
index d2b9204542a..d8e6357cc9c 100644
--- a/web/core/services/inbox/inbox-issue.service.ts
+++ b/web/core/services/inbox/inbox-issue.service.ts
@@ -76,22 +76,6 @@ export class InboxIssueService extends APIService {
});
}
- async fetchDescriptionBinary(workspaceSlug: string, projectId: string, inboxIssueId: string): Promise {
- return this.get(
- `/api/workspaces/${workspaceSlug}/projects/${projectId}/inbox-issues/${inboxIssueId}/description/`,
- {
- headers: {
- "Content-Type": "application/octet-stream",
- },
- responseType: "arraybuffer",
- }
- )
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
async updateDescriptionBinary(
workspaceSlug: string,
projectId: string,
diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts
index 6825a29b8ad..5a9854062f9 100644
--- a/web/core/services/issue/issue.service.ts
+++ b/web/core/services/issue/issue.service.ts
@@ -390,19 +390,6 @@ export class IssueService extends APIService {
});
}
- async fetchDescriptionBinary(workspaceSlug: string, projectId: string, issueId: string): Promise {
- return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/description/`, {
- headers: {
- "Content-Type": "application/octet-stream",
- },
- responseType: "arraybuffer",
- })
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
async updateDescriptionBinary(
workspaceSlug: string,
projectId: string,
diff --git a/web/core/store/issue/issue-details/issue.store.ts b/web/core/store/issue/issue-details/issue.store.ts
index 89a9c84d556..342c3adca67 100644
--- a/web/core/store/issue/issue-details/issue.store.ts
+++ b/web/core/store/issue/issue-details/issue.store.ts
@@ -58,14 +58,14 @@ export class IssueStore implements IIssueStore {
issueArchiveService;
issueDraftService;
- constructor(rootStore: IIssueRootStore) {
+ constructor(rootStore: IIssueRootStore, rootIssueDetailStore: IIssueDetail) {
makeObservable(this, {
fetchingIssueDetails: observable.ref,
localDBIssueDescription: observable.ref,
});
// root store
this.rootIssueStore = rootStore;
- this.rootIssueDetailStore = rootStore.issueDetail;
+ this.rootIssueDetailStore = rootIssueDetailStore;
// services
this.issueService = new IssueService();
this.issueArchiveService = new IssueArchiveService();
diff --git a/web/core/store/issue/issue-details/root.store.ts b/web/core/store/issue/issue-details/root.store.ts
index b4370903394..6a98715d9b4 100644
--- a/web/core/store/issue/issue-details/root.store.ts
+++ b/web/core/store/issue/issue-details/root.store.ts
@@ -192,7 +192,7 @@ export class IssueDetail implements IIssueDetail {
// store
this.rootIssueStore = rootStore;
- this.issue = new IssueStore(rootStore);
+ this.issue = new IssueStore(rootStore, this);
this.reaction = new IssueReactionStore(this);
this.attachment = new IssueAttachmentStore(rootStore);
this.activity = new IssueActivityStore(rootStore.rootStore as RootStore);
From 3792699080301bc7ad7cad4b407fab0728f4bc51 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Tue, 12 Nov 2024 16:17:04 +0530
Subject: [PATCH 21/21] chore: update live server error handling
---
live/src/server.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/live/src/server.ts b/live/src/server.ts
index 43d926abd4a..ff9977e0a37 100644
--- a/live/src/server.ts
+++ b/live/src/server.ts
@@ -66,12 +66,12 @@ app.post("/resolve-document-conflicts", (req, res) => {
res.status(400).send({
message: "Missing required fields",
});
- throw new Error("Missing required fields");
+ return;
}
const resolvedDocument = resolveDocumentConflicts(req.body);
res.status(200).json(resolvedDocument);
} catch (error) {
- console.error("error", error);
+ manualLogger.error("Error in /resolve-document-conflicts endpoint:", error);
res.status(500).send({
message: "Internal server error",
});