From e781ab988073af70d7e0464f616bba22652ed273 Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Thu, 24 Oct 2024 14:26:26 +0530
Subject: [PATCH 01/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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",
});
From 0b64cae0c96fa51d85663d150304a64d05f554c9 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Thu, 28 Nov 2024 16:54:08 +0530
Subject: [PATCH 22/26] fix: merge conflicts resolved from preview
---
apiserver/plane/app/serializers/draft.py | 4 ---
apiserver/plane/app/views/workspace/draft.py | 31 ++++----------------
2 files changed, 5 insertions(+), 30 deletions(-)
diff --git a/apiserver/plane/app/serializers/draft.py b/apiserver/plane/app/serializers/draft.py
index 96bec925c11..c3ac41b845c 100644
--- a/apiserver/plane/app/serializers/draft.py
+++ b/apiserver/plane/app/serializers/draft.py
@@ -268,12 +268,8 @@ class DraftIssueDetailSerializer(DraftIssueSerializer):
description_binary = serializers.CharField()
class Meta(DraftIssueSerializer.Meta):
-<<<<<<< HEAD
fields = DraftIssueSerializer.Meta.fields + [
"description_html",
"description_binary",
]
-=======
- fields = DraftIssueSerializer.Meta.fields + ["description_html"]
->>>>>>> 378e896bf063546518aa7bc96a0d8f55d49703db
read_only_fields = fields
diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py
index b4a11244187..cafa6046623 100644
--- a/apiserver/plane/app/views/workspace/draft.py
+++ b/apiserver/plane/app/views/workspace/draft.py
@@ -9,18 +9,8 @@
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
-<<<<<<< HEAD
from django.http import StreamingHttpResponse
-from django.db.models import (
- Q,
- UUIDField,
- Value,
- Subquery,
- OuterRef,
-)
-=======
from django.db.models import Q, UUIDField, Value, Subquery, OuterRef
->>>>>>> 378e896bf063546518aa7bc96a0d8f55d49703db
from django.db.models.functions import Coalesce
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
@@ -338,10 +328,7 @@ def create_draft_to_issue(self, request, slug, draft_id):
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,
- )
+ return Response({"error": "Issue not found"}, status=404)
binary_data = issue.description_binary
def stream_data():
@@ -364,9 +351,7 @@ def update_description(self, request, slug, pk):
base64_description = issue.description_binary
# convert to base64 string
if base64_description:
- base64_description = base64.b64encode(base64_description).decode(
- "utf-8"
- )
+ base64_description = base64.b64encode(base64_description).decode("utf-8")
data = {
"original_document": base64_description,
"updates": request.data.get("description_binary"),
@@ -381,16 +366,10 @@ def update_description(self, request, slug, pk):
)
if response.status_code == 200:
- issue.description = response.json().get(
- "description", issue.description
- )
+ issue.description = response.json().get("description", issue.description)
issue.description_html = response.json().get("description_html")
- response_description_binary = response.json().get(
- "description_binary"
- )
- issue.description_binary = base64.b64decode(
- response_description_binary
- )
+ response_description_binary = response.json().get("description_binary")
+ issue.description_binary = base64.b64decode(response_description_binary)
issue.save()
def stream_data():
From 60b58eff2e2e0cae5c689996f8406c27c5c14c91 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Mon, 2 Dec 2024 16:02:00 +0530
Subject: [PATCH 23/26] fix: live server post request
---
live/src/server.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/live/src/server.ts b/live/src/server.ts
index ff9977e0a37..007e138fa8e 100644
--- a/live/src/server.ts
+++ b/live/src/server.ts
@@ -59,7 +59,7 @@ router.ws("/collaboration", (ws, req) => {
}
});
-app.post("/resolve-document-conflicts", (req, res) => {
+router.post("/resolve-document-conflicts", (req, res) => {
const { original_document, updates } = req.body as TResolveConflictsRequestBody;
try {
if (original_document === undefined || updates === undefined) {
From 9e5b10412d81bc11517e87fd07a480d035ff41a5 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Mon, 2 Dec 2024 16:27:08 +0530
Subject: [PATCH 24/26] chore: remove yjs from the live server
---
live/package.json | 5 +----
yarn.lock | 35 +++++------------------------------
2 files changed, 6 insertions(+), 34 deletions(-)
diff --git a/live/package.json b/live/package.json
index a4fed4434d0..bdebf125ee5 100644
--- a/live/package.json
+++ b/live/package.json
@@ -38,10 +38,7 @@
"morgan": "^1.10.0",
"pino-http": "^10.3.0",
"pino-pretty": "^11.2.2",
- "uuid": "^10.0.0",
- "y-prosemirror": "^1.2.9",
- "y-protocols": "^1.0.6",
- "yjs": "^13.6.14"
+ "uuid": "^10.0.0"
},
"devDependencies": {
"@babel/cli": "^7.25.6",
diff --git a/yarn.lock b/yarn.lock
index d2a3b5ef9fd..5cc0bc6db00 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11166,16 +11166,7 @@ streamx@^2.15.0, streamx@^2.20.0:
optionalDependencies:
bare-events "^2.2.0"
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -11263,14 +11254,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0:
dependencies:
safe-buffer "~5.2.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -12478,16 +12462,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -12542,7 +12517,7 @@ y-indexeddb@^9.0.12:
dependencies:
lib0 "^0.2.74"
-y-prosemirror@^1.2.5, y-prosemirror@^1.2.9:
+y-prosemirror@^1.2.5:
version "1.2.13"
resolved "https://registry.yarnpkg.com/y-prosemirror/-/y-prosemirror-1.2.13.tgz#1355c82474e66e4fb9644ed0104cf0af5a9766b1"
integrity sha512-eW5/rJcYhqlrqyPLTaGIaPSt7YS6f2wwbV5wXlUkY8rERY0tSJPoumsPD7UWwtXk9C5TdTsvwaIrdOoD1QLbYA==
@@ -12594,7 +12569,7 @@ yargs@^17.0.0, yargs@^17.7.2:
y18n "^5.0.5"
yargs-parser "^21.1.1"
-yjs@^13.6.14, yjs@^13.6.15:
+yjs@^13.6.15:
version "13.6.20"
resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.20.tgz#da878412688f107dc03faa4fc3cff37736fe5dfa"
integrity sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==
From 0e4d7138ea126b0e663231bd6628a28c1179e021 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Fri, 27 Dec 2024 15:33:50 +0530
Subject: [PATCH 25/26] fix: rich text issue description editor
---
.../editors/document/collaborative-editor.tsx | 4 +--
.../rich-text/collaborative-editor.tsx | 1 +
...s => use-collaborative-document-editor.ts} | 4 +--
.../use-collaborative-rich-text-editor.ts | 2 ++
.../src/core/types/collaboration-hook.ts | 25 +++++--------
.../rich-text-editor/collaborative-editor.tsx | 36 +++++++++----------
.../collaborative-read-only-editor.tsx | 7 ++--
.../components/issues/description-input.tsx | 9 +++++
8 files changed, 43 insertions(+), 45 deletions(-)
rename packages/editor/src/core/hooks/{use-collaborative-editor.ts => use-collaborative-document-editor.ts} (94%)
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 44c18c2f680..2c6dc38e706 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";
@@ -44,7 +44,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
}
// use document editor
- const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
+ const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeDocumentEditor({
disabledExtensions,
editable,
editorClassName,
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
index ee0e1bb9319..b4da8624d44 100644
--- a/packages/editor/src/core/components/editors/rich-text/collaborative-editor.tsx
+++ b/packages/editor/src/core/components/editors/rich-text/collaborative-editor.tsx
@@ -30,6 +30,7 @@ const CollaborativeRichTextEditor = (props: ICollaborativeRichTextEditor) => {
const { editor } = useCollaborativeRichTextEditor({
disabledExtensions,
editorClassName,
+ editable: true,
fileHandler,
forwardedRef,
id,
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 94%
rename from packages/editor/src/core/hooks/use-collaborative-editor.ts
rename to packages/editor/src/core/hooks/use-collaborative-document-editor.ts
index 4abf7d6d1ff..17eb9ea108b 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 {
onTransaction,
disabledExtensions,
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 c33a27407ee..9a49bce2581 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
@@ -13,6 +13,7 @@ import { TCollaborativeRichTextEditorHookProps } from "@/types";
export const useCollaborativeRichTextEditor = (props: TCollaborativeRichTextEditorHookProps) => {
const {
disabledExtensions,
+ editable,
editorClassName,
editorProps = {},
extensions,
@@ -51,6 +52,7 @@ export const useCollaborativeRichTextEditor = (props: TCollaborativeRichTextEdit
const editor = useEditor({
id,
disabledExtensions,
+ editable,
editorProps,
editorClassName,
enableHistory: false,
diff --git a/packages/editor/src/core/types/collaboration-hook.ts b/packages/editor/src/core/types/collaboration-hook.ts
index 2e8c9a0e723..bf59514674d 100644
--- a/packages/editor/src/core/types/collaboration-hook.ts
+++ b/packages/editor/src/core/types/collaboration-hook.ts
@@ -27,13 +27,9 @@ type TCollaborativeEditorHookCommonProps = {
extensions?: Extensions;
handleEditorReady?: (value: boolean) => void;
id: string;
- realtimeConfig: TRealtimeConfig;
- serverHandler?: TServerHandler;
- user: TUserDetails;
};
type TCollaborativeEditorHookProps = TCollaborativeEditorHookCommonProps & {
- onTransaction?: () => void;
embedHandler?: TEmbedConfig;
fileHandler: TFileHandler;
forwardedRef?: React.MutableRefObject;
@@ -48,24 +44,19 @@ type TCollaborativeReadOnlyEditorHookProps = TCollaborativeEditorHookCommonProps
mentionHandler: TReadOnlyMentionHandler;
};
-export type TCollaborativeRichTextEditorHookProps = TCollaborativeEditorHookProps & {
- onChange: (updatedDescription: Uint8Array) => void;
- value: Uint8Array;
-};
-
-export type TCollaborativeRichTextReadOnlyEditorHookProps = TCollaborativeReadOnlyEditorHookProps & {
- value: Uint8Array;
-};
-
export type TCollaborativeDocumentEditorHookProps = TCollaborativeEditorHookProps & {
+ onTransaction?: () => void;
embedHandler?: TEmbedConfig;
realtimeConfig: TRealtimeConfig;
serverHandler?: TServerHandler;
user: TUserDetails;
};
-export type TCollaborativeDocumentReadOnlyEditorHookProps = TCollaborativeReadOnlyEditorHookProps & {
- realtimeConfig: TRealtimeConfig;
- serverHandler?: TServerHandler;
- user: TUserDetails;
+export type TCollaborativeRichTextEditorHookProps = TCollaborativeEditorHookProps & {
+ onChange: (updatedDescription: Uint8Array) => void;
+ value: Uint8Array;
+};
+
+export type TCollaborativeRichTextReadOnlyEditorHookProps = TCollaborativeReadOnlyEditorHookProps & {
+ value: Uint8Array;
};
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 f52f6a9aa7e..a44cd0c9a2a 100644
--- a/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
+++ b/web/core/components/editor/rich-text-editor/collaborative-editor.tsx
@@ -2,12 +2,14 @@ import React, { forwardRef } from "react";
// editor
import { CollaborativeRichTextEditorWithRef, EditorRefApi, ICollaborativeRichTextEditor } from "@plane/editor";
// types
-import { IUserLite } from "@plane/types";
+import { TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
+// components
+import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { cn } from "@/helpers/common.helper";
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
-import { useMember, useMention, useUser } from "@/hooks/store";
+import { useEditorMention } from "@/hooks/use-editor-mention";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
@@ -15,30 +17,20 @@ import { useFileSize } from "@/plane-web/hooks/use-file-size";
interface Props extends Omit {
key: string;
projectId: string;
+ searchMentionCallback: (payload: TSearchEntityRequestPayload) => Promise;
uploadFile: (file: File) => Promise;
workspaceId: string;
workspaceSlug: string;
}
export const CollaborativeRichTextEditor = forwardRef((props, ref) => {
- const { containerClassName, workspaceSlug, workspaceId, projectId, uploadFile, ...rest } = props;
- // store hooks
- const { data: currentUser } = useUser();
- const {
- getUserDetails,
- project: { getProjectMemberIds },
- } = useMember();
+ const { containerClassName, workspaceSlug, workspaceId, projectId, searchMentionCallback, uploadFile, ...rest } =
+ props;
// editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
- // derived values
- const projectMemberIds = getProjectMemberIds(projectId);
- const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
- // use-mention
- const { mentionHighlights, mentionSuggestions } = useMention({
- workspaceSlug,
- projectId,
- members: projectMemberDetails,
- user: currentUser,
+ // use editor mention
+ const { fetchMentions } = useEditorMention({
+ searchEntity: async (payload) => await searchMentionCallback(payload),
});
// file size
const { maxFileSize } = useFileSize();
@@ -55,8 +47,12 @@ export const CollaborativeRichTextEditor = forwardRef((prop
workspaceSlug,
})}
mentionHandler={{
- highlights: mentionHighlights,
- suggestions: mentionSuggestions,
+ searchCallback: async (query) => {
+ const res = await fetchMentions(query);
+ if (!res) throw new Error("Failed in fetching mentions");
+ return res;
+ },
+ renderComponent: (props) => ,
}}
{...rest}
containerClassName={cn("relative pl-3", containerClassName)}
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 0ad9bd2dd1e..da8449bad1c 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
@@ -7,11 +7,12 @@ import {
} from "@plane/editor";
// plane ui
import { Loader } from "@plane/ui";
+// components
+import { EditorMentionsRoot } from "@/components/editor";
// 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";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
@@ -30,8 +31,6 @@ export const CollaborativeRichTextReadOnlyEditor = React.forwardRef<
EditorReadOnlyRefApi,
RichTextReadOnlyEditorWrapperProps
>(({ descriptionBinary: savedDescriptionBinary, descriptionHTML, projectId, workspaceSlug, ...props }, ref) => {
- // store hooks
- const { mentionHighlights } = useMention({});
// editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
@@ -57,7 +56,7 @@ export const CollaborativeRichTextReadOnlyEditor = React.forwardRef<
workspaceSlug,
})}
mentionHandler={{
- highlights: mentionHighlights,
+ renderComponent: (props) => ,
}}
{...props}
// overriding the containerClassName to add relative class passed
diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx
index f100143b12b..01217b1095f 100644
--- a/web/core/components/issues/description-input.tsx
+++ b/web/core/components/issues/description-input.tsx
@@ -15,8 +15,11 @@ import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
// hooks
import { useWorkspace } from "@/hooks/store";
import { useIssueDescription } from "@/hooks/use-issue-description";
+// plane web services
+import { WorkspaceService } from "@/plane-web/services";
// services
import { FileService } from "@/services/file.service";
+const workspaceService = new WorkspaceService();
const fileService = new FileService();
export type IssueDescriptionInputProps = {
@@ -109,6 +112,12 @@ export const IssueDescriptionInput: FC = observer((p
placeholder={placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)}
projectId={projectId}
ref={editorRef}
+ searchMentionCallback={async (payload) =>
+ await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
+ ...payload,
+ project_id: projectId?.toString() ?? "",
+ })
+ }
uploadFile={async (file) => {
try {
const { asset_id } = await fileService.uploadProjectAsset(
From 312ce38d94cfc10dbfed93ddf61af3a8d6bd6b11 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal
Date: Fri, 27 Dec 2024 15:40:27 +0530
Subject: [PATCH 26/26] fix: merge conflicts resolved from preview
---
yarn.lock | 347 +++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 249 insertions(+), 98 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 5cc0bc6db00..70544c6a05c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -851,7 +851,7 @@
"@babel/plugin-transform-modules-commonjs" "^7.25.9"
"@babel/plugin-transform-typescript" "^7.25.9"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
@@ -952,6 +952,11 @@
react-confetti "^6.1.0"
strip-ansi "^7.1.0"
+"@colors/colors@1.6.0", "@colors/colors@^1.6.0":
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0"
+ integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==
+
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
@@ -959,6 +964,15 @@
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
+"@dabh/diagnostics@^2.0.2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a"
+ integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==
+ dependencies:
+ colorspace "1.1.x"
+ enabled "2.0.x"
+ kuler "^2.0.0"
+
"@daybrush/utils@^1.1.1", "@daybrush/utils@^1.13.0", "@daybrush/utils@^1.4.0", "@daybrush/utils@^1.6.0", "@daybrush/utils@^1.7.1":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@daybrush/utils/-/utils-1.13.0.tgz#ea70a60864130da476406fdd1d465e3068aea0ff"
@@ -1651,10 +1665,10 @@
prop-types "^15.8.1"
react-is "^18.3.1"
-"@next/env@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.18.tgz#ccbcf906f0123a37cff6edc1effd524d635fd395"
- integrity sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==
+"@next/env@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.22.tgz#8898ae47595badbfacebfc1585f42a4e06a97301"
+ integrity sha512-EQ6y1QeNQglNmNIXvwP/Bb+lf7n9WtgcWvtoFsHquVLCJUuxRs+6SfZ5EK0/EqkkLex4RrDySvKgKNN7PXip7Q==
"@next/eslint-plugin-next@14.2.18":
version "14.2.18"
@@ -1663,50 +1677,50 @@
dependencies:
glob "10.3.10"
-"@next/swc-darwin-arm64@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz#273d490a11a271044d2c060339d24b99886a75c1"
- integrity sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==
-
-"@next/swc-darwin-x64@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.18.tgz#b347cd799584ff79f7b69e5be8833b270fd6c51d"
- integrity sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==
-
-"@next/swc-linux-arm64-gnu@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.18.tgz#6d59d4d5b4b76e453512e1e2b5ad12a2b75930cd"
- integrity sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==
-
-"@next/swc-linux-arm64-musl@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.18.tgz#a45624d5bc5a5abd640d97ac9c3e43562f81e258"
- integrity sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==
-
-"@next/swc-linux-x64-gnu@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.18.tgz#04a98935cb94e301a3477bbe7754e8f86b44c0e3"
- integrity sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==
-
-"@next/swc-linux-x64-musl@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.18.tgz#018415234d5f0086bdabdbd92bdc85223c35cffa"
- integrity sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==
-
-"@next/swc-win32-arm64-msvc@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.18.tgz#a2e26c9858147a6438c583b37f8b7374a124fbea"
- integrity sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==
-
-"@next/swc-win32-ia32-msvc@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.18.tgz#1694e7366df9f34925822b8bd3296c1ea94b0eb3"
- integrity sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==
-
-"@next/swc-win32-x64-msvc@14.2.18":
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.18.tgz#91d56970f0e484c7e4f8a9f11635d82552e0fce1"
- integrity sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==
+"@next/swc-darwin-arm64@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.22.tgz#2b3fcb42247ba951b19a48fc03f1d6fe65629baa"
+ integrity sha512-HUaLiehovgnqY4TMBZJ3pDaOsTE1spIXeR10pWgdQVPYqDGQmHJBj3h3V6yC0uuo/RoY2GC0YBFRkOX3dI9WVQ==
+
+"@next/swc-darwin-x64@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.22.tgz#11ecc609e9530b3edf8ddfd1fd3bd6aca4e1bfda"
+ integrity sha512-ApVDANousaAGrosWvxoGdLT0uvLBUC+srqOcpXuyfglA40cP2LBFaGmBjhgpxYk5z4xmunzqQvcIgXawTzo2uQ==
+
+"@next/swc-linux-arm64-gnu@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.22.tgz#4c08dd223e50c348f561af2285e27fb326ffabbf"
+ integrity sha512-3O2J99Bk9aM+d4CGn9eEayJXHuH9QLx0BctvWyuUGtJ3/mH6lkfAPRI4FidmHMBQBB4UcvLMfNf8vF0NZT7iKw==
+
+"@next/swc-linux-arm64-musl@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.22.tgz#0b285336f145887d421b3762f3d7c75f847ec1b3"
+ integrity sha512-H/hqfRz75yy60y5Eg7DxYfbmHMjv60Dsa6IWHzpJSz4MRkZNy5eDnEW9wyts9bkxwbOVZNPHeb3NkqanP+nGPg==
+
+"@next/swc-linux-x64-gnu@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.22.tgz#a936b6cfea0364571102f0389c6368d6acf3e294"
+ integrity sha512-LckLwlCLcGR1hlI5eiJymR8zSHPsuruuwaZ3H2uudr25+Dpzo6cRFjp/3OR5UYJt8LSwlXv9mmY4oI2QynwpqQ==
+
+"@next/swc-linux-x64-musl@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.22.tgz#0359497840d0b7d8c095d0d9735bc6aec68cef5d"
+ integrity sha512-qGUutzmh0PoFU0fCSu0XYpOfT7ydBZgDfcETIeft46abPqP+dmePhwRGLhFKwZWxNWQCPprH26TjaTxM0Nv8mw==
+
+"@next/swc-win32-arm64-msvc@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.22.tgz#9fd249d49ffccf3388400ab24472c432cdd04c24"
+ integrity sha512-K6MwucMWmIvMb9GlvT0haYsfIPxfQD8yXqxwFy4uLFMeXIb2TcVYQimxkaFZv86I7sn1NOZnpOaVk5eaxThGIw==
+
+"@next/swc-win32-ia32-msvc@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.22.tgz#70d8d5a48e78c7382c3e0544af28c2788ca6b551"
+ integrity sha512-5IhDDTPEbzPR31ZzqHe90LnNe7BlJUZvC4sA1thPJV6oN5WmtWjZ0bOYfNsyZx00FJt7gggNs6SrsX0UEIcIpA==
+
+"@next/swc-win32-x64-msvc@14.2.22":
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.22.tgz#b034f544c1346093a235f6bba46497a1ba344fc1"
+ integrity sha512-nvRaB1PyG4scn9/qNzlkwEwLzuoPH3Gjp7Q/pLuwUgOTt1oPMlnCI3A3rgkt+eZnU71emOiEv/mR201HoURPGg==
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
version "2.1.8-no-fsevents.3"
@@ -4222,6 +4236,11 @@
dependencies:
"@types/node" "*"
+"@types/triple-beam@^1.3.2":
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
+ integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
+
"@types/trusted-types@^2.0.7":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
@@ -4259,7 +4278,7 @@
dependencies:
"@types/node" "*"
-"@types/zxcvbn@^4.4.4":
+"@types/zxcvbn@^4.4.4", "@types/zxcvbn@^4.4.5":
version "4.4.5"
resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.5.tgz#8ce8623ed7a36e3a76d1c0b539708dfb2e859bc0"
integrity sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==
@@ -4900,6 +4919,11 @@ async-lock@^1.3.1:
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.1.tgz#56b8718915a9b68b10fce2f2a9a3dddf765ef53f"
integrity sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==
+async@^3.2.3:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
+ integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
+
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -4951,6 +4975,15 @@ axe-core@^4.10.0:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df"
integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==
+axios@^1.4.0:
+ version "1.7.9"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
+ integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==
+ dependencies:
+ follow-redirects "^1.15.6"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
+
axios@^1.7.2, axios@^1.7.4:
version "1.7.8"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.8.tgz#1997b1496b394c21953e68c14aaa51b7b5de3d6e"
@@ -5402,11 +5435,6 @@ clone@^2.1.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
-clsx@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
- integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
-
clsx@^2.0.0, clsx@^2.1.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
@@ -5427,6 +5455,13 @@ cmdk@^1.0.0:
"@radix-ui/react-primitive" "^2.0.0"
use-sync-external-store "^1.2.2"
+color-convert@^1.9.3:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@@ -5434,12 +5469,17 @@ color-convert@^2.0.1:
dependencies:
color-name "~1.1.4"
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
color-name@^1.0.0, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-color-string@^1.9.0, color-string@^1.9.1:
+color-string@^1.6.0, color-string@^1.9.0, color-string@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
@@ -5447,6 +5487,14 @@ color-string@^1.9.0, color-string@^1.9.1:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
+color@^3.1.3:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
+ integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
+ dependencies:
+ color-convert "^1.9.3"
+ color-string "^1.6.0"
+
color@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
@@ -5460,6 +5508,14 @@ colorette@^2.0.10, colorette@^2.0.7:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
+colorspace@1.1.x:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243"
+ integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==
+ dependencies:
+ color "^3.1.3"
+ text-hex "1.0.x"
+
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -5896,17 +5952,10 @@ data-view-byte-offset@^1.0.0:
es-errors "^1.3.0"
is-data-view "^1.0.1"
-date-fns@^2.30.0:
- version "2.30.0"
- resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
- integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
- dependencies:
- "@babel/runtime" "^7.21.0"
-
-date-fns@^3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
- integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
+date-fns@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14"
+ integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==
dateformat@^4.6.3:
version "4.6.3"
@@ -6270,6 +6319,11 @@ emoji-regex@^9.2.2:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+enabled@2.0.x:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
+ integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
+
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -6695,7 +6749,7 @@ eslint-visitor-keys@^4.2.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
-eslint@8:
+eslint@8.57.1:
version "8.57.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
@@ -6841,10 +6895,10 @@ express-ws@^5.0.2:
dependencies:
ws "^7.4.6"
-express@^4.20.0:
- version "4.21.1"
- resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281"
- integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==
+express@^4.21.2:
+ version "4.21.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32"
+ integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
@@ -6865,7 +6919,7 @@ express@^4.20.0:
methods "~1.1.2"
on-finished "2.4.1"
parseurl "~1.3.3"
- path-to-regexp "0.1.10"
+ path-to-regexp "0.1.12"
proxy-addr "~2.0.7"
qs "6.13.0"
range-parser "~1.2.1"
@@ -6963,6 +7017,11 @@ fdir@^6.2.0:
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689"
integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==
+fecha@^4.2.0:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
+ integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
+
fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
@@ -6982,6 +7041,13 @@ file-selector@^2.1.0:
dependencies:
tslib "^2.7.0"
+file-stream-rotator@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz#007019e735b262bb6c6f0197e58e5c87cb96cec3"
+ integrity sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==
+ dependencies:
+ moment "^2.29.1"
+
filesize@^10.0.12:
version "10.1.6"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.6.tgz#31194da825ac58689c0bce3948f33ce83aabd361"
@@ -7070,6 +7136,11 @@ flatted@^3.2.9:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27"
integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==
+fn.name@1.x.x:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
+ integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
+
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
@@ -8230,6 +8301,11 @@ kleur@^4.0.3, kleur@^4.1.4:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
+kuler@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
+ integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
+
language-subtag-registry@^0.3.20:
version "0.3.23"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7"
@@ -8366,6 +8442,18 @@ lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+logform@^2.7.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/logform/-/logform-2.7.0.tgz#cfca97528ef290f2e125a08396805002b2d060d1"
+ integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==
+ dependencies:
+ "@colors/colors" "1.6.0"
+ "@types/triple-beam" "^1.3.2"
+ fecha "^4.2.0"
+ ms "^2.1.1"
+ safe-stable-stringify "^2.3.1"
+ triple-beam "^1.3.0"
+
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -8907,6 +8995,11 @@ module-details-from-path@^1.0.3:
resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b"
integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==
+moment@^2.29.1:
+ version "2.30.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
+ integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
+
morgan@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7"
@@ -8982,12 +9075,12 @@ next-themes@^0.2.1:
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.2.1.tgz#0c9f128e847979daf6c67f70b38e6b6567856e45"
integrity sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==
-next@^14.2.12:
- version "14.2.18"
- resolved "https://registry.yarnpkg.com/next/-/next-14.2.18.tgz#1750bc0c3dda644d48be530d1b0e71ad92025cf8"
- integrity sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==
+next@^14.2.20:
+ version "14.2.22"
+ resolved "https://registry.yarnpkg.com/next/-/next-14.2.22.tgz#0cd664916ef4c725f31fa812d870348cffd0115b"
+ integrity sha512-Ps2caobQ9hlEhscLPiPm3J3SYhfwfpMqzsoCMZGWxt9jBRK9hoBZj2A37i8joKhsyth2EuVKDVJCTF5/H4iEDw==
dependencies:
- "@next/env" "14.2.18"
+ "@next/env" "14.2.22"
"@swc/helpers" "0.5.5"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
@@ -8995,15 +9088,15 @@ next@^14.2.12:
postcss "8.4.31"
styled-jsx "5.1.1"
optionalDependencies:
- "@next/swc-darwin-arm64" "14.2.18"
- "@next/swc-darwin-x64" "14.2.18"
- "@next/swc-linux-arm64-gnu" "14.2.18"
- "@next/swc-linux-arm64-musl" "14.2.18"
- "@next/swc-linux-x64-gnu" "14.2.18"
- "@next/swc-linux-x64-musl" "14.2.18"
- "@next/swc-win32-arm64-msvc" "14.2.18"
- "@next/swc-win32-ia32-msvc" "14.2.18"
- "@next/swc-win32-x64-msvc" "14.2.18"
+ "@next/swc-darwin-arm64" "14.2.22"
+ "@next/swc-darwin-x64" "14.2.22"
+ "@next/swc-linux-arm64-gnu" "14.2.22"
+ "@next/swc-linux-arm64-musl" "14.2.22"
+ "@next/swc-linux-x64-gnu" "14.2.22"
+ "@next/swc-linux-x64-musl" "14.2.22"
+ "@next/swc-win32-arm64-msvc" "14.2.22"
+ "@next/swc-win32-ia32-msvc" "14.2.22"
+ "@next/swc-win32-x64-msvc" "14.2.22"
no-case@^3.0.4:
version "3.0.4"
@@ -9228,6 +9321,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
+one-time@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
+ integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
+ dependencies:
+ fn.name "1.x.x"
+
onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
@@ -9424,10 +9524,10 @@ path-scurry@^1.10.1, path-scurry@^1.11.1, path-scurry@^1.6.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
-path-to-regexp@0.1.10:
- version "0.1.10"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
- integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
+path-to-regexp@0.1.12:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
+ integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
path-type@^4.0.0:
version "4.0.0"
@@ -10419,7 +10519,7 @@ read-cache@^1.0.0:
dependencies:
pify "^2.3.0"
-readable-stream@^3.1.1, readable-stream@^3.4.0:
+readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
@@ -11126,6 +11226,11 @@ split2@^4.0.0:
resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
+stack-trace@0.0.x:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
+ integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==
+
stacktrace-parser@^0.1.10:
version "0.1.10"
resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a"
@@ -11395,16 +11500,16 @@ tabbable@^6.0.0:
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
-tailwind-merge@^1.14.0:
- version "1.14.0"
- resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b"
- integrity sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==
-
tailwind-merge@^2.0.0:
version "2.5.5"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.5.tgz#98167859b856a2a6b8d2baf038ee171b9d814e39"
integrity sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==
+tailwind-merge@^2.5.5:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
+ integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
+
tailwindcss-animate@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
@@ -11539,6 +11644,11 @@ text-decoder@^1.1.0:
resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.1.tgz#e173f5121d97bfa3ff8723429ad5ba92e1ead67e"
integrity sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==
+text-hex@1.0.x:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
+ integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
+
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -11677,6 +11787,11 @@ trim-lines@^3.0.0:
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
+triple-beam@^1.3.0, triple-beam@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984"
+ integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==
+
trough@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f"
@@ -11916,7 +12031,7 @@ typescript@5.3.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
-typescript@^5.6.2:
+typescript@^5.3.3:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
@@ -12457,6 +12572,42 @@ which@^2.0.1, which@^2.0.2:
dependencies:
isexe "^2.0.0"
+winston-daily-rotate-file@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz#8cd94800025490e47c00ec892b655a5821f4266d"
+ integrity sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==
+ dependencies:
+ file-stream-rotator "^0.6.1"
+ object-hash "^3.0.0"
+ triple-beam "^1.4.1"
+ winston-transport "^4.7.0"
+
+winston-transport@^4.7.0, winston-transport@^4.9.0:
+ version "4.9.0"
+ resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9"
+ integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==
+ dependencies:
+ logform "^2.7.0"
+ readable-stream "^3.6.2"
+ triple-beam "^1.3.0"
+
+winston@^3.17.0:
+ version "3.17.0"
+ resolved "https://registry.yarnpkg.com/winston/-/winston-3.17.0.tgz#74b8665ce9b4ea7b29d0922cfccf852a08a11423"
+ integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==
+ dependencies:
+ "@colors/colors" "^1.6.0"
+ "@dabh/diagnostics" "^2.0.2"
+ async "^3.2.3"
+ is-stream "^2.0.0"
+ logform "^2.7.0"
+ one-time "^1.0.0"
+ readable-stream "^3.4.0"
+ safe-stable-stringify "^2.3.1"
+ stack-trace "0.0.x"
+ triple-beam "^1.3.0"
+ winston-transport "^4.9.0"
+
word-wrap@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"