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"