-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WEB-1435] chore: conflict free issue description #6120
base: preview
Are you sure you want to change the base?
Conversation
…re/conflict-free-issue-description
…ict-free-issue-description
…keplane/plane into chore/conflict-free-issue-description
…re/conflict-free-issue-description
…ict-free-issue-description
WalkthroughThe pull request introduces several enhancements across multiple files, primarily focusing on the management of issue descriptions in binary format. Key changes include the addition of a new Changes
Possibly related PRs
Suggested labels
Suggested reviewers
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 16
🧹 Outside diff range and nitpick comments (26)
web/core/hooks/use-page-fallback.ts (1)
Line range hint
32-38
: Consider adding error handling for binary conversionWhile the use of
convertBinaryDataToBase64String
is a good improvement, consider adding try-catch error handling around the conversion and update process to gracefully handle any potential conversion failures.- const encodedBinary = convertBinaryDataToBase64String(binary); - - await updatePageDescription({ - description_binary: encodedBinary, - description_html: html, - description: json, - }); + try { + const encodedBinary = convertBinaryDataToBase64String(binary); + await updatePageDescription({ + description_binary: encodedBinary, + description_html: html, + description: json, + }); + } catch (error) { + console.error('Failed to convert or update description:', error); + // Consider adding error handling/reporting mechanism + }web/core/components/editor/rich-text-editor/collaborative-editor.tsx (1)
35-36
: Filter out undefined member details
getUserDetails
may return undefined if a user is not found. To prevent potential errors, filter out any undefined values when mappingprojectMemberIds
toprojectMemberDetails
.Apply this diff to improve the code:
const projectMemberDetails = projectMemberIds - ?.map((id) => getUserDetails(id) as IUserLite); + ?.map((id) => getUserDetails(id)) + .filter((user): user is IUserLite => user !== undefined);apiserver/plane/app/views/workspace/draft.py (2)
388-389
: Provide informative error message on server errorWhen the external service does not return a 200 response, the method returns a 500 error without any message. Consider adding an error message to help clients understand the cause of the failure.
Apply this diff to enhance the error response:
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {"error": "Failed to update the issue description"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + )
334-339
: Eliminate code duplication instream_data
functionThe
stream_data
function is identical in bothretrieve_description
andupdate_description
methods. Refactor this code to eliminate duplication by extracting it into a separate method.Here's how you might refactor the code:
def _stream_description_data(self, binary_data): if binary_data: yield binary_data else: yield b""Then, update both methods to use this helper:
# In retrieve_description - def stream_data(): - if binary_data: - yield binary_data - else: - yield b"" - response = StreamingHttpResponse( - stream_data(), content_type="application/octet-stream" + self._stream_description_data(binary_data), content_type="application/octet-stream" ) # In update_description - def stream_data(): - if issue.description_binary: - yield issue.description_binary - else: - yield b"" - response = StreamingHttpResponse( - stream_data(), content_type="application/octet-stream" + self._stream_description_data(issue.description_binary), content_type="application/octet-stream" )Also applies to: 375-380
packages/editor/src/core/components/editors/rich-text/index.ts (1)
1-2
: Ensure consistent export orderFor better readability and consistency, consider grouping and ordering the export statements logically, perhaps by placing collaborative editors together and standard editors afterwards.
Suggested change:
export * from "./collaborative-editor"; export * from "./collaborative-read-only-editor"; export * from "./editor"; export * from "./read-only-editor";packages/editor/src/core/hooks/use-collaborative-rich-text-read-only-editor.ts (2)
46-49
: Consider making SideMenuExtension configuration more flexible.The SideMenuExtension has hardcoded values for
aiEnabled
anddragDropEnabled
. Consider making these configurable through props for better reusability.- SideMenuExtension({ - aiEnabled: false, - dragDropEnabled: true, - }), + SideMenuExtension({ + aiEnabled: props.aiEnabled ?? false, + dragDropEnabled: props.dragDropEnabled ?? true, + }),
63-65
: Consider exposing the provider in the return value.For better integration with the collaborative features, consider exposing the provider instance in the return value. This would allow parent components to access provider state or handle reconnection scenarios.
return { editor, + provider, };
packages/editor/src/core/types/collaboration-hook.ts (1)
Line range hint
22-34
: Add JSDoc comments to improve type documentationThe base type
TCollaborativeEditorHookCommonProps
would benefit from documentation explaining the purpose and usage of each property.Consider adding JSDoc:
+/** + * Common properties shared across all collaborative editor hooks + */ type TCollaborativeEditorHookCommonProps = { + /** List of extensions to disable in the editor */ disabledExtensions: TExtensions[]; + /** CSS class name for the editor container */ editorClassName: string; // ... rest of the properties };web/core/hooks/use-issue-description.ts (3)
9-13
: Consider making updateDescription required for non-readonly modeThe optional
updateDescription
parameter suggests the hook can be used without update capability, but this isn't clearly documented.Consider splitting into separate types:
type TReadOnlyArgs = { descriptionBinary: string | null; descriptionHTML: string | null; }; type TEditableArgs = TReadOnlyArgs & { updateDescription: (data: string) => Promise<ArrayBuffer>; };
20-34
: Add retry mechanism for description updatesThe error handling for
updateDescription
could be more robust by implementing retries for transient failures.Consider implementing a retry mechanism:
const resolveConflictsAndUpdateDescription = useCallback( async (encodedDescription: string, editorRef: EditorRefApi | null) => { if (!updateDescription) return; + const MAX_RETRIES = 3; + let attempts = 0; try { - const conflictFreeEncodedDescription = await updateDescription(encodedDescription); + while (attempts < MAX_RETRIES) { + try { + const conflictFreeEncodedDescription = await updateDescription(encodedDescription); + const decodedDescription = conflictFreeEncodedDescription + ? new Uint8Array(conflictFreeEncodedDescription) + : new Uint8Array(); + editorRef?.setProviderDocument(decodedDescription); + break; + } catch (error) { + attempts++; + if (attempts === MAX_RETRIES) throw error; + await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); + } + } } catch (error) { console.error("Error while updating description", error); } }, [updateDescription] );
36-48
: Add cleanup for state managementThe effect should clean up the state when the component unmounts.
Consider adding cleanup:
useEffect(() => { if (descriptionBinary) return; if (savedDescriptionBinary) { const savedDescriptionBuffer = convertBase64StringToBinaryData(savedDescriptionBinary); const decodedSavedDescription = savedDescriptionBuffer ? new Uint8Array(savedDescriptionBuffer) : new Uint8Array(); setDescriptionBinary(decodedSavedDescription); } else { const decodedDescriptionHTML = getBinaryDataFromRichTextEditorHTMLString(descriptionHTML ?? "<p></p>"); setDescriptionBinary(decodedDescriptionHTML); } + return () => { + setDescriptionBinary(null); + }; }, [descriptionBinary, descriptionHTML, savedDescriptionBinary]);packages/editor/src/core/components/editors/rich-text/collaborative-editor.tsx (2)
30-41
: Consider memoizing the hook configuration objectThe configuration object passed to
useCollaborativeRichTextEditor
is recreated on every render.Consider using
useMemo
:+ const editorConfig = React.useMemo( + () => ({ disabledExtensions, editorClassName, fileHandler, forwardedRef, id, mentionHandler, onChange, placeholder, tabIndex, value, + }), + [disabledExtensions, editorClassName, fileHandler, forwardedRef, id, mentionHandler, onChange, placeholder, tabIndex, value] + ); - const { editor } = useCollaborativeRichTextEditor({ - disabledExtensions, - editorClassName, - fileHandler, - forwardedRef, - id, - mentionHandler, - onChange, - placeholder, - tabIndex, - value, - }); + const { editor } = useCollaborativeRichTextEditor(editorConfig);
51-63
: Consider error boundary for editor failuresThe editor rendering could potentially fail during collaborative operations.
Consider wrapping the editor content with an error boundary to gracefully handle runtime errors:
+ import { ErrorBoundary } from '@/components/error-boundary'; return ( <EditorContainer displayConfig={displayConfig} editor={editor} editorContainerClassName={editorContainerClassName} id={id} > + <ErrorBoundary fallback={<div>Error loading editor</div>}> <EditorBubbleMenu editor={editor} /> <div className="flex flex-col"> <EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} /> </div> + </ErrorBoundary> </EditorContainer> );packages/editor/src/core/components/editors/rich-text/collaborative-read-only-editor.tsx (1)
37-41
: Consider memoizing the editorContainerClassName.Since
getEditorClassNames
is called on every render with static values, consider memoizing the result usinguseMemo
.- const editorContainerClassName = getEditorClassNames({ + const editorContainerClassName = React.useMemo(() => getEditorClassNames({ noBorder: true, borderOnFocus: false, containerClassName, - }); + }), [containerClassName]);web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx (1)
64-64
: Consider making the padding configurable.The hardcoded
pl-3
class in the container className override might not be suitable for all use cases.- containerClassName={cn(props.containerClassName, "relative pl-3")} + containerClassName={cn(props.containerClassName, "relative", props.noPadding ? "" : "pl-3")}live/src/core/lib/page.ts (1)
50-51
: Consider adding retry mechanism for HTML transformation.The HTML to binary transformation is critical but lacks retry logic. Consider adding retries for transient failures.
const pageDetails = await pageService.fetchDetails(workspaceSlug, projectId, pageId, cookie); -const contentBinary = getBinaryDataFromDocumentEditorHTMLString(pageDetails.description_html ?? "<p></p>"); +const contentBinary = await retry( + () => getBinaryDataFromDocumentEditorHTMLString(pageDetails.description_html ?? "<p></p>"), + { retries: 3, delay: 1000 } +);Also applies to: 54-54
live/src/server.ts (1)
62-79
: Verify error handling completeness in conflict resolution endpointThe endpoint implementation looks solid with proper error handling, but consider a few improvements:
- Add request body validation for the structure of
original_document
andupdates
- Consider adding request size limits to prevent DOS attacks
- Add specific error types in the response for better client-side handling
router.post("/resolve-document-conflicts", (req, res) => { + // Add request size validation + if (req.get('content-length') > 1024 * 1024) { // 1MB limit + res.status(413).send({ + error: 'payload_too_large', + message: 'Request body too large' + }); + return; + } + const { original_document, updates } = req.body as TResolveConflictsRequestBody; try { if (original_document === undefined || updates === undefined) { res.status(400).send({ + error: 'missing_fields', message: "Missing required fields", }); return; } + // Add structure validation + if (!Array.isArray(updates)) { + res.status(400).send({ + error: 'invalid_updates', + message: "Updates must be an array" + }); + return; + } const resolvedDocument = resolveDocumentConflicts(req.body); res.status(200).json(resolvedDocument); } catch (error) { manualLogger.error("Error in /resolve-document-conflicts endpoint:", error); res.status(500).send({ + error: 'internal_error', message: "Internal server error", }); } });web/core/components/issues/peek-overview/issue-detail.tsx (1)
102-117
: Strengthen the description_binary checkThe current check
!== undefined
might allownull
values through. Consider a more robust check.-{issue.description_binary !== undefined && ( +{issue.description_binary != null && (Also, consider adding a loading state:
+{issue.description_binary === undefined && ( + <div className="-ml-3 h-32 w-full animate-pulse rounded-md bg-gray-100" /> +)}packages/editor/src/core/hooks/use-read-only-editor.ts (1)
Line range hint
100-108
: Consider validating providerDocument stateWhile the implementation works, it might benefit from additional validation to ensure the providerDocument is in a valid state before encoding.
-const documentBinary = providerDocument ? Y.encodeStateAsUpdate(providerDocument) : null; +const documentBinary = providerDocument?.store ? Y.encodeStateAsUpdate(providerDocument) : null;Also, consider adding JSDoc documentation for the new prop:
/** * Optional Y.Doc instance for direct document state access. * When provided, takes precedence over provider.document for binary encoding. */ providerDocument?: Y.Doc;web/core/services/page/project-page.service.ts (1)
Line range hint
131-142
: Enhance error handling for binary data fetchingWhile the type specification and headers are correct, the error handling could be improved to handle binary response errors more gracefully.
Consider this enhancement:
async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise<ArrayBuffer> { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, { headers: { "Content-Type": "application/octet-stream", }, responseType: "arraybuffer", }) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + // Handle binary response errors + const errorMessage = error.response?.headers?.['content-type']?.includes('application/json') + ? JSON.parse(new TextDecoder().decode(error.response.data)) + : { detail: 'Failed to fetch binary description' }; + throw errorMessage; }); }web/core/components/pages/editor/page-root.tsx (1)
53-55
: Enhance error message with more contextWhile the error handling is good, the error message could be more informative by including the context of the operation.
Consider this enhancement:
if (!page.id) { - throw new Error("Required fields missing while fetching binary description"); + throw new Error(`Required field 'id' is missing while fetching binary description for page "${page.name || 'unknown'}"`); }web/core/components/inbox/content/issue-root.tsx (2)
Line range hint
116-131
: Fix inconsistent event tracking payloadThe failure case is incorrectly tracking the event as "SUCCESS".
} catch { setToast({ title: "Issue update failed", type: TOAST_TYPE.ERROR, message: "Issue update failed", }); captureIssueEvent({ eventName: "Inbox issue updated", - payload: { state: "SUCCESS", element: "Inbox" }, + payload: { state: "FAILED", element: "Inbox" }, updates: { changed_property: Object.keys(data).join(","), change_details: Object.values(data).join(","), }, path: pathname, }); }
Line range hint
47-57
: Prevent potential memory leak in setTimeoutThe setTimeout should be cleaned up when the component unmounts.
useEffect(() => { + let timeoutId: NodeJS.Timeout; if (isSubmitting === "submitted") { setShowAlert(false); - setTimeout(async () => { + timeoutId = setTimeout(async () => { setIsSubmitting("saved"); }, 3000); } else if (isSubmitting === "submitting") { setShowAlert(true); } + return () => { + if (timeoutId) clearTimeout(timeoutId); + }; }, [isSubmitting, setShowAlert, setIsSubmitting]);web/core/store/inbox/inbox-issue.store.ts (1)
180-198
: Consider enhancing error handling and state management.The implementation looks good but could benefit from some improvements:
- Consider preserving the previous description state for rollback on failure
- The error message could include more details about what went wrong
Here's a suggested improvement:
updateIssueDescription = async (descriptionBinary: string): Promise<ArrayBuffer> => { + const previousDescription = this.issue.description_binary; try { if (!this.issue.id) throw new Error("Issue id is missing"); const res = await this.inboxIssueService.updateDescriptionBinary( this.workspaceSlug, this.projectId, this.issue.id, { description_binary: descriptionBinary, } ); set(this.issue, "description_binary", descriptionBinary); // fetching activity this.fetchIssueActivity(); return res; - } catch { - throw new Error("Failed to update local issue description"); + } catch (error) { + set(this.issue, "description_binary", previousDescription); + throw new Error(`Failed to update issue description: ${error.message}`); } };web/core/components/issues/issue-detail/root.tsx (1)
128-134
: Consider enhancing error handling.While the error handling is present, it could be improved by:
- Including more specific error information
- Adding error logging
updateDescription: async (workspaceSlug, projectId, issueId, descriptionBinary) => { try { return await updateIssueDescription(workspaceSlug, projectId, issueId, descriptionBinary); } catch (error) { - throw new Error("Failed to update issue description"); + console.error("Error updating issue description:", error); + throw new Error(`Failed to update issue description: ${error.message}`); } },apiserver/plane/app/serializers/issue.py (1)
645-656
: Add docstring and consider field selection optimization.The
IssueDetailSerializer
would benefit from:
- Documentation explaining the purpose and usage of binary description
- Field selection optimization to avoid loading both description formats when not needed
Consider applying these enhancements:
class IssueDetailSerializer(IssueSerializer): + """ + Serializer for detailed issue representation including binary description support. + + The binary description field allows storing rich text content in a conflict-free format, + encoded as base64 to ensure safe transmission in JSON. + """ description_html = serializers.CharField() - description_binary = Base64BinaryField() + description_binary = Base64BinaryField( + max_size=10 * 1024 * 1024, # 10MB limit + help_text="Base64 encoded binary representation of the description" + ) 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 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Optimize field selection based on request + if self.context.get('request'): + fields = self.context['request'].query_params.get('fields', '').split(',') + if 'description_binary' not in fields: + self.fields.pop('description_binary', None) + if 'description_html' not in fields: + self.fields.pop('description_html', None)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
⛔ Files ignored due to path filters (1)
yarn.lock
is excluded by!**/yarn.lock
,!**/*.lock
📒 Files selected for processing (61)
apiserver/plane/app/serializers/draft.py
(1 hunks)apiserver/plane/app/serializers/issue.py
(2 hunks)apiserver/plane/app/urls/intake.py
(1 hunks)apiserver/plane/app/urls/issue.py
(2 hunks)apiserver/plane/app/urls/workspace.py
(1 hunks)apiserver/plane/app/views/intake/base.py
(3 hunks)apiserver/plane/app/views/issue/archive.py
(2 hunks)apiserver/plane/app/views/issue/base.py
(3 hunks)apiserver/plane/app/views/workspace/draft.py
(2 hunks)apiserver/plane/settings/common.py
(1 hunks)live/.prettierignore
(1 hunks)live/.prettierrc
(1 hunks)live/package.json
(1 hunks)live/src/core/helpers/page.ts
(0 hunks)live/src/core/lib/page.ts
(3 hunks)live/src/core/resolve-conflicts.ts
(1 hunks)live/src/server.ts
(4 hunks)packages/editor/src/core/components/editors/document/collaborative-editor.tsx
(2 hunks)packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx
(2 hunks)packages/editor/src/core/components/editors/editor-wrapper.tsx
(2 hunks)packages/editor/src/core/components/editors/rich-text/collaborative-editor.tsx
(1 hunks)packages/editor/src/core/components/editors/rich-text/collaborative-read-only-editor.tsx
(1 hunks)packages/editor/src/core/components/editors/rich-text/index.ts
(1 hunks)packages/editor/src/core/helpers/yjs-utils.ts
(1 hunks)packages/editor/src/core/helpers/yjs.ts
(0 hunks)packages/editor/src/core/hooks/use-collaborative-document-editor.ts
(3 hunks)packages/editor/src/core/hooks/use-collaborative-document-read-only-editor.ts
(2 hunks)packages/editor/src/core/hooks/use-collaborative-rich-text-editor.ts
(1 hunks)packages/editor/src/core/hooks/use-collaborative-rich-text-read-only-editor.ts
(1 hunks)packages/editor/src/core/hooks/use-editor.ts
(4 hunks)packages/editor/src/core/hooks/use-read-only-editor.ts
(3 hunks)packages/editor/src/core/providers/custom-collaboration-provider.ts
(1 hunks)packages/editor/src/core/providers/index.ts
(1 hunks)packages/editor/src/core/types/collaboration-hook.ts
(3 hunks)packages/editor/src/core/types/editor.ts
(3 hunks)packages/editor/src/core/types/index.ts
(1 hunks)packages/editor/src/index.ts
(2 hunks)packages/editor/src/lib.ts
(1 hunks)packages/types/src/issues/issue.d.ts
(1 hunks)web/core/components/core/modals/gpt-assistant-popover.tsx
(1 hunks)web/core/components/editor/rich-text-editor/collaborative-editor.tsx
(1 hunks)web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx
(1 hunks)web/core/components/editor/rich-text-editor/index.ts
(1 hunks)web/core/components/inbox/content/issue-root.tsx
(9 hunks)web/core/components/inbox/modals/create-modal/issue-description.tsx
(1 hunks)web/core/components/issues/description-input.tsx
(1 hunks)web/core/components/issues/issue-detail/main-content.tsx
(2 hunks)web/core/components/issues/issue-detail/root.tsx
(4 hunks)web/core/components/issues/peek-overview/issue-detail.tsx
(2 hunks)web/core/components/issues/peek-overview/root.tsx
(3 hunks)web/core/components/pages/editor/page-root.tsx
(1 hunks)web/core/components/profile/activity/activity-list.tsx
(1 hunks)web/core/components/profile/activity/profile-activity-list.tsx
(1 hunks)web/core/hooks/use-issue-description.ts
(1 hunks)web/core/hooks/use-page-fallback.ts
(2 hunks)web/core/services/inbox/inbox-issue.service.ts
(2 hunks)web/core/services/issue/issue.service.ts
(2 hunks)web/core/services/page/project-page.service.ts
(1 hunks)web/core/store/inbox/inbox-issue.store.ts
(3 hunks)web/core/store/issue/issue-details/issue.store.ts
(5 hunks)web/core/store/issue/issue-details/root.store.ts
(2 hunks)
💤 Files with no reviewable changes (2)
- packages/editor/src/core/helpers/yjs.ts
- live/src/core/helpers/page.ts
✅ Files skipped from review due to trivial changes (8)
- apiserver/plane/settings/common.py
- live/.prettierrc
- packages/editor/src/core/types/index.ts
- packages/editor/src/core/providers/index.ts
- packages/editor/src/lib.ts
- live/.prettierignore
- web/core/components/editor/rich-text-editor/index.ts
- web/core/components/profile/activity/profile-activity-list.tsx
🔇 Additional comments (65)
web/core/hooks/use-page-fallback.ts (2)
3-3
: LGTM! Clean import organization
The addition of convertBinaryDataToBase64String
import is well-organized and clearly indicates its purpose.
11-11
: Great improvement in type safety!
The change from Promise<any>
to Promise<ArrayBuffer>
provides better type safety and clarity about the expected return type.
Let's verify that this type change is consistently applied across the codebase:
✅ Verification successful
Let me gather more information to verify the type consistency.
Let me check the actual implementation to verify the type consistency.
Type change is correctly implemented and consistent
The change from Promise<any>
to Promise<ArrayBuffer>
is properly aligned with the implementation. The service method fetchDescriptionBinary
returns Promise<ArrayBuffer>
and all usages across the codebase handle the binary data appropriately, including the conversion to Uint8Array
where needed.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check for any remaining Promise<any> types in related files
# and verify consistent usage of ArrayBuffer return type
# Search for fetchPageDescription implementations
ast-grep --pattern 'fetchPageDescription = async () => { $$$ }'
# Search for related type definitions
rg -l "fetchPageDescription.*Promise"
Length of output: 141
Script:
#!/bin/bash
# Let's check the actual implementation and usage of fetchPageDescription
rg "fetchPageDescription" -A 5 -B 5
# Also check for any ArrayBuffer type usage in related contexts
rg "ArrayBuffer" -A 2 -B 2
Length of output: 73874
Script:
# Check the implementation of fetchDescriptionBinary to verify return type
rg -A 5 "fetchDescriptionBinary" web/core/services/page/project-page.service.ts
# Also check for any calls to this method
rg -A 2 "fetchDescriptionBinary\("
Length of output: 1542
packages/editor/src/core/providers/custom-collaboration-provider.ts (1)
48-54
: Ensure hasSynced
is managed correctly
The hasSynced
property is initialized but not updated within the class. Ensure that hasSynced
is set to true
when synchronization is complete so that documentUpdateHandler
functions as intended.
apiserver/plane/app/views/intake/base.py (4)
616-616
: Handle exception when issue does not exist
Using get()
without handling the DoesNotExist
exception can lead to server errors if the issue does not exist. Modify to use filter().first()
and handle the None
case.
Apply this diff to fix the issue:
def update_description(self, request, slug, project_id, pk):
- issue = Issue.objects.get(
+ issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=pk
+ ).first()
+ if issue is None:
+ return Response({"error": "Issue not found"}, status=404)
627-627
: Validate 'updates' field in request data
The updates
field from request.data
is used without validation. Ensure that description_binary
is properly validated to prevent potential errors or security issues.
665-665
: Provide informative error message on server error
When the external service fails to process the request, a generic 500 error is returned without any message. Add an error message to clarify the cause for clients.
Apply this diff to enhance the error response:
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ return Response(
+ {"error": "Failed to update the issue description"},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ )
600-605
: Eliminate code duplication in stream_data
function
The stream_data
function is duplicated in both retrieve_description
and update_description
methods. Refactor to extract this into a shared helper method.
Refactor example:
def _stream_description_data(self, binary_data):
if binary_data:
yield binary_data
else:
yield b""
Then, update both methods accordingly.
Also applies to: 652-656
apiserver/plane/app/views/issue/base.py (4)
708-710
: Handle exception when issue does not exist
Using get()
without exception handling can cause server errors if the issue does not exist. Use filter().first()
and check for None
to handle this case appropriately.
Apply this diff to fix the issue:
def update_description(self, request, slug, project_id, pk):
- issue = Issue.issue_objects.get(
+ issue = Issue.issue_objects.filter(
workspace__slug=slug, project_id=project_id, pk=pk
+ ).first()
+ if issue is None:
+ return Response({"error": "Issue not found"}, status=404)
719-719
: Validate 'updates' field in request data
Ensure that request.data.get("description_binary")
is validated before use to prevent potential security risks or data processing errors.
757-758
: Provide informative error message on server error
When the external service fails, the method returns a generic 500 error without explanation. Providing an error message can help clients understand the failure.
Apply this diff to enhance the error response:
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ return Response(
+ {"error": "Failed to update the issue description"},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ )
693-698
: Eliminate code duplication in stream_data
function
The stream_data
function is duplicated in both retrieve_description
and update_description
. Consolidate this code into a single helper method to improve maintainability.
Refactor example:
def _stream_description_data(self, binary_data):
if binary_data:
yield binary_data
else:
yield b""
Update both methods to use the helper.
Also applies to: 743-748
packages/editor/src/index.ts (2)
13-14
: Export statements are appropriate
The addition of CollaborativeRichTextEditorWithRef
and CollaborativeRichTextReadOnlyEditorWithRef
enhances the editor's functionality.
30-30
: Update export path for Yjs utilities
The change from yjs
to yjs-utils
accurately reflects the updated helper utilities.
packages/editor/src/core/components/editors/editor-wrapper.tsx (1)
1-1
: LGTM! Type improvement using AnyExtension.
The change from Extension<any, any>[]
to AnyExtension[]
is a good improvement that better represents the type and removes the use of any
.
Also applies to: 15-15
live/src/core/resolve-conflicts.ts (1)
8-17
: LGTM! Well-structured type definitions
The types clearly define the contract for request and response, making the API surface explicit and type-safe.
packages/editor/src/core/types/collaboration-hook.ts (1)
49-69
: LGTM! Well-structured type hierarchy
The separation between rich text and document editor types is clean and follows good inheritance patterns. The reuse of common props reduces duplication.
apiserver/plane/app/urls/intake.py (1)
56-65
: LGTM! Verify endpoint permissions
The new endpoint follows RESTful conventions and maintains consistency with existing patterns. The path structure and HTTP methods are appropriate for handling issue descriptions.
Let's verify the permissions setup:
✅ Verification successful
Based on the code analysis, I can now generate the final response:
Permissions are properly configured for the inbox issue description endpoints
The IntakeIssueViewSet inherits from BaseViewSet and both retrieve_description and update_description methods are explicitly decorated with @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
, which follows the same permission pattern as other issue-related operations in the codebase. This ensures that:
- Only authenticated users with appropriate roles (Admin, Member, or Guest) can access these endpoints
- The permissions are consistent with the rest of the issue management functionality
- The implementation follows the project's established permission patterns
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check if permissions are properly configured for the new endpoint
# Search for permission classes in the IntakeIssueViewSet
ast-grep --pattern 'class IntakeIssueViewSet {
$$$
retrieve_description($$$) {
$$$
}
update_description($$$) {
$$$
}
}'
# Check for any existing permission tests
rg -l "test.*inbox.*issue.*description" -g "test_*.py"
Length of output: 196
Script:
#!/bin/bash
# Let's try a different approach to find the IntakeIssueViewSet implementation
# Find the file containing IntakeIssueViewSet
fd "IntakeIssueViewSet" --type f
# Search for permission_classes in the found files
rg "permission_classes" -A 5 -B 5
# Look for any test files related to inbox issues
rg "class.*Test.*Inbox.*Issue" -g "test_*.py"
Length of output: 71240
Script:
#!/bin/bash
# Let's find the IntakeIssueViewSet implementation and check its permissions
# Search for IntakeIssueViewSet class definition
rg "class IntakeIssueViewSet" -A 10
# Search for retrieve_description and update_description methods
rg "def (retrieve|update)_description" -A 10
Length of output: 7733
Script:
#!/bin/bash
# Let's check the permissions for IntakeIssueViewSet and its base class
# Find permission_classes in IntakeIssueViewSet file
rg "permission_classes" apiserver/plane/app/views/intake/base.py -B 5 -A 5
# Check if there are any specific permission overrides for description methods
rg "def get_permissions" apiserver/plane/app/views/intake/base.py -A 10
Length of output: 148
Script:
#!/bin/bash
# Let's check the entire IntakeIssueViewSet file content to understand its permissions
# Display the content of the file
cat apiserver/plane/app/views/intake/base.py
# Also check if it inherits from any base classes
rg "class.*BaseViewSet.*:" -A 5
Length of output: 49244
packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx (1)
11-11
: LGTM! Verify hook renaming consistency
The hook renaming improves clarity by being more specific about its purpose (Document + ReadOnly). The implementation remains functionally unchanged.
Let's verify the renaming consistency across the codebase:
Also applies to: 40-40
✅ Verification successful
Hook renaming is consistent across the codebase
The verification shows that:
- The old hook name
useReadOnlyCollaborativeEditor
has been completely removed - The new naming pattern is consistently applied across similar hooks:
- Document editors:
useCollaborativeDocumentEditor
,useCollaborativeDocumentReadOnlyEditor
- Rich text editors:
useCollaborativeRichTextEditor
,useCollaborativeRichTextReadOnlyEditor
- Document editors:
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check for any remaining old hook references
# Search for any remaining references to the old hook name
rg "useReadOnlyCollaborativeEditor"
# Search for consistent naming pattern in other similar hooks
rg "useCollaborative.*Editor"
Length of output: 2346
packages/editor/src/core/components/editors/rich-text/collaborative-editor.tsx (2)
14-28
: LGTM! Props interface is well-structured
The component's props interface is comprehensive and includes all necessary configuration options for a collaborative editor. Default values are properly handled.
66-70
: LGTM! Proper ref forwarding implementation
The ref forwarding implementation follows React best practices and properly types the forwarded ref.
packages/editor/src/core/components/editors/rich-text/collaborative-read-only-editor.tsx (2)
14-35
: LGTM! Props handling and hook usage are well-implemented.
The component properly destructures props with appropriate default values and passes them correctly to the custom hook.
60-70
: LGTM! Proper ref forwarding implementation.
The component correctly implements ref forwarding with proper TypeScript types and display name.
packages/editor/src/core/components/editors/document/collaborative-editor.tsx (1)
11-11
: Verify the impact of switching to useCollaborativeDocumentEditor.
The change from useCollaborativeEditor
to useCollaborativeDocumentEditor
seems to be a refactor to use a more specific hook. Let's verify that all consumers are updated accordingly.
Also applies to: 46-46
✅ Verification successful
The hook change appears to be correctly implemented
The search results show that the old hook useCollaborativeEditor
is no longer used anywhere in the codebase, and the new useCollaborativeDocumentEditor
hook is consistently used in the collaborative editor component. The implementation and all references are aligned.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for any remaining usage of the old hook
rg "useCollaborativeEditor" --type ts --type tsx
# Search for all usages of the new hook to ensure consistency
rg "useCollaborativeDocumentEditor" --type ts --type tsx
Length of output: 161
Script:
#!/bin/bash
# Search for any remaining usage of the old hook without file type restrictions
rg "useCollaborativeEditor"
# Search for all usages of the new hook to ensure consistency
rg "useCollaborativeDocumentEditor"
Length of output: 575
web/core/components/editor/rich-text-editor/collaborative-read-only-editor.tsx (1)
19-27
: LGTM! Well-structured type definition.
The type definition properly extends the base interface while omitting props that will be handled internally.
packages/editor/src/core/hooks/use-collaborative-document-read-only-editor.ts (2)
10-10
: LGTM! Clear and descriptive renaming.
The hook and type renaming from useReadOnlyCollaborativeEditor
to useCollaborativeDocumentReadOnlyEditor
better reflects its purpose and context.
Also applies to: 12-12
85-85
: LGTM! Proper provider document propagation.
The providerDocument
property is correctly passed from provider.document
to the useReadOnlyEditor
hook.
live/src/core/lib/page.ts (4)
1-5
: LGTM! More specific and descriptive imports.
The imports have been updated to use more specific function names that better describe their purpose with the document editor.
15-15
: LGTM! Improved error handling.
The error message for invalid updatedDescription
is now more precise and informative.
Also applies to: 18-18
69-69
: LGTM! Proper fallback mechanism.
The code correctly handles empty binary data by falling back to HTML transformation.
Also applies to: 73-73
26-26
: Verify binary data transformation and persistence.
The code correctly transforms and persists binary data, but we should verify the data integrity across the transformation pipeline.
Also applies to: 34-34
✅ Verification successful
Let me analyze the binary data transformation by looking at the implementation of these functions.
Let me search for the implementation of the binary data transformation functions and their error handling.
Let me check for error handling in the page.ts file where these transformations are used.
Based on the code analysis, I can now provide a final response about the binary data transformation verification:
Binary data transformation has proper error handling and logging in place
The transformation pipeline is well-structured with:
- Clear conversion steps from HTML -> JSON -> Y.Doc -> Uint8Array
- Error handling that logs transformation failures and re-throws errors for proper error propagation
- Proper fallback handling with null coalescing for HTML input (
descriptionHTML ?? "<p></p>"
)
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check for potential data loss or corruption in binary transformations
# Look for error handling patterns in binary data transformations
# Search for binary data handling patterns
rg -A 3 'getAllDocumentFormatsFromDocumentEditorBinaryData|getBinaryDataFromDocumentEditorHTMLString'
# Look for error handling patterns around binary data
rg -A 3 'catch.*error.*binary'
Length of output: 67370
Script:
#!/bin/bash
# Search for the implementation of binary data transformation functions
ast-grep --pattern 'export const getAllDocumentFormatsFromDocumentEditorBinaryData = $_'
# Search for error handling around binary data transformations
rg -A 5 'catch.*error.*binary|error.*binary.*transform'
Length of output: 67537
Script:
#!/bin/bash
# Search for the implementation of binary data transformation functions
ast-grep --pattern 'export const getBinaryDataFromDocumentEditorHTMLString = $_'
# Search for error handling around these transformations
rg -A 5 'catch.*error.*transform|error.*transform.*binary'
Length of output: 1060
Script:
#!/bin/bash
# Look for error handling in page.ts around the binary transformations
rg -A 5 'try.*getAllDocumentFormatsFromDocumentEditorBinaryData|try.*getBinaryDataFromDocumentEditorHTMLString' live/src/core/lib/page.ts
# Also check for any error logging or handling related to these functions
rg -A 5 'manualLogger.error.*transform|catch.*transform' live/src/core/lib/page.ts
Length of output: 382
packages/types/src/issues/issue.d.ts (1)
53-53
: LGTM! Type definition aligns with binary data support.
The addition of the optional description_binary
field to the TIssue
type properly supports the new binary description functionality.
web/core/components/inbox/modals/create-modal/issue-description.tsx (1)
13-13
: Verify the simplified import path for RichTextEditor
The import path has been simplified from a specific path to a more general one. While this suggests better organization, we should ensure that the correct component is being imported.
✅ Verification successful
Import path change for RichTextEditor is valid
The simplified import path @/components/editor
is correct. The codebase shows that:
- The RichTextEditor component is properly exported from the editor components directory
- The component is used consistently across multiple files with the same import pattern
- The implementation in
web/core/components/editor/rich-text-editor/editor.tsx
contains the full functionality needed, including mentions, file handling, and editor flagging
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Verify the export of RichTextEditor from @/components/editor
# Expected: Find the export statement for RichTextEditor
# Check for RichTextEditor exports
ast-grep --pattern 'export { RichTextEditor }'
Length of output: 48
Script:
#!/bin/bash
# Let's try different approaches to find RichTextEditor
# 1. Look for any export of RichTextEditor (including default exports)
ast-grep --pattern 'export $_ RichTextEditor'
# 2. Look for the component definition
ast-grep --pattern 'const RichTextEditor = $_'
# 3. Search for files that might contain the editor component
fd editor
# 4. Search for RichTextEditor references in the codebase
rg "RichTextEditor" -A 2
Length of output: 25098
packages/editor/src/core/hooks/use-collaborative-document-editor.ts (2)
14-14
: LGTM: Hook renamed for better clarity
The hook has been renamed from useCollaborativeEditor to useCollaborativeDocumentEditor, which better reflects its purpose.
105-105
: Verify the impact of adding providerDocument
The addition of providerDocument to the editor configuration enhances the integration with the Hocuspocus provider.
✅ Verification successful
Based on the search results, I can generate the final response as we have sufficient information about the usage of providerDocument
in the codebase.
The addition of providerDocument is consistent with the codebase's patterns
The providerDocument
property is consistently used across multiple editor hooks in the codebase, including read-only and collaborative variants. It's properly integrated with Y.js (Y.Doc) for collaborative editing functionality, making this addition a valid enhancement.
- Used in base hooks:
use-editor.ts
anduse-read-only-editor.ts
as an optional prop - Consistently passed as
provider.document
in all collaborative variants - Properly utilized for document state management with Y.js operations
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check for other uses of providerDocument in the codebase
# Expected: Find related usage and potential impacts
rg "providerDocument" -A 3
Length of output: 3835
live/src/server.ts (2)
13-14
: LGTM: Clean import organization
The imports are well-organized, grouping related functionality together.
101-101
: LGTM: Clear logging message
The log message clearly indicates the successful closure of WebSocket connections.
web/core/services/inbox/inbox-issue.service.ts (2)
2-2
: LGTM: Clean import addition
The addition of TDocumentPayload
type is properly imported and used in the new method.
79-96
: Consider using PATCH instead of POST for updates
While the implementation is functionally correct, using POST for updates deviates from RESTful conventions. POST is typically used for creating new resources, while PATCH is more appropriate for partial updates.
Consider this modification:
- return this.post(
+ return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inbox-issues/${inboxIssueId}/description/`,
data,
{
responseType: "arraybuffer",
}
)
Let's verify if other description update endpoints follow the same pattern:
web/core/components/issues/peek-overview/issue-detail.tsx (1)
15-17
: LGTM: Clean import reorganization
The hooks import has been properly categorized under "plane web hooks".
packages/editor/src/core/hooks/use-read-only-editor.ts (1)
35-35
: LGTM: Clean type addition
The optional providerDocument prop is properly typed as Y.Doc.
web/core/components/issues/issue-detail/main-content.tsx (2)
25-26
: LGTM: Debounced duplicate issues hook integration
The addition of the debounced hook for duplicate issues is a good optimization to prevent excessive API calls.
117-132
: 🛠️ Refactor suggestion
Improve the conditional rendering check for description_binary
The current check description_binary !== undefined
might allow null
values to pass through. Consider using a more strict check.
Apply this change:
-{issue.description_binary !== undefined && (
+{issue.description_binary != null && (
Also, verify that all required props are properly typed to prevent runtime errors.
✅ Verification successful
Use strict null check for description_binary
Based on the codebase analysis, description_binary
is defined as nullable in multiple places:
- In the database schema as
BinaryField(null=True)
- In TypeScript types as
description_binary?: string
The current check !== undefined
would allow null
values to pass through, which could cause issues. The suggested != null
check is more appropriate as it handles both undefined
and null
cases.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check for IssueDescriptionInput prop types definition
ast-grep --pattern 'interface $name {
$$$
descriptionBinary: $_
$$$
}'
Length of output: 78
Script:
#!/bin/bash
# Let's try a different approach to find the IssueDescriptionInput component and its prop types
ast-grep --pattern 'type $name = {
$$$
descriptionBinary: $_
$$$
}'
# Also search for the component definition
ast-grep --pattern 'const IssueDescriptionInput = $_'
# And let's check how description_binary is used in other places
rg "description_binary" -A 2 -B 2
Length of output: 25640
packages/editor/src/core/types/editor.ts (3)
128-128
: LGTM: Type definition for value property
The optional string type for the value property is correctly defined.
139-143
: LGTM: Well-structured collaborative editor interface
The ICollaborativeRichTextEditor
interface correctly:
- Omits properties that would conflict with collaborative editing
- Uses
Uint8Array
for binary data handling - Maintains type safety with proper method signatures
175-177
: LGTM: Read-only collaborative editor interface
The ICollaborativeRichTextReadOnlyEditor
interface properly extends the base interface while maintaining read-only constraints.
apiserver/plane/app/urls/workspace.py (1)
213-222
: LGTM: Well-structured URL pattern for draft issue descriptions
The endpoint is properly designed with:
- Secure identifier usage (UUID)
- Clear RESTful methods (GET/POST)
- Consistent naming convention
web/core/components/inbox/content/issue-root.tsx (1)
76-76
: Verify the empty fetch method implementation
The fetch
method is empty. If it's intentionally empty, consider removing it or adding a comment explaining why.
✅ Verification successful
Let me gather more context about this component and its usage to better understand the purpose of this fetch method.
Let me check how this issueOperations
object is being used in the component and if there are any patterns of similar implementations elsewhere.
Based on the search results, I can now generate the final response as I have enough context about the issueOperations
and its usage in the codebase.
Empty fetch method is intentional for inbox issue operations
The empty fetch
method in issue-root.tsx
is intentional as it's part of the inbox-specific issue operations interface. Unlike the main issue detail components which require fetching, inbox issues are already loaded through the inbox state management, making the fetch operation unnecessary.
Key observations:
- The empty fetch is part of the
issueOperations
object used specifically in the inbox context - Other components like
peek-overview/root.tsx
andissue-detail/root.tsx
have proper fetch implementations - The inbox component still implements other required operations like update, remove, etc.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for usages of the fetch method
rg -A 2 "fetch: async \(\)"
Length of output: 283
Script:
#!/bin/bash
# Get more context around the fetch method and the component structure
rg -B 5 -A 10 "fetch: async \(\)" web/core/components/inbox/content/issue-root.tsx
# Look for any related hooks or context that might explain this pattern
ast-grep --pattern 'const $_ = use$_($$$)'
Length of output: 620
Script:
#!/bin/bash
# Search for TIssueOperations type definition
rg "type TIssueOperations" -A 5
# Search for other usages of issueOperations
rg "issueOperations" -B 2 -A 2
# Look for similar patterns in other components
ast-grep --pattern 'const $_ = useMemo(() => ({
fetch: $_,
$$$
}), $_)'
Length of output: 51320
web/core/components/profile/activity/activity-list.tsx (1)
11-11
: LGTM!
The import path simplification aligns with the codebase's refactoring efforts while maintaining the same functionality.
web/core/components/core/modals/gpt-assistant-popover.tsx (1)
12-12
: LGTM!
The import path simplification is consistent with the codebase's refactoring efforts while preserving the component's functionality.
web/core/store/inbox/inbox-issue.store.ts (1)
29-29
: LGTM!
The new method is properly declared in the interface with clear documentation and correctly registered as a MobX action.
Also applies to: 82-82
apiserver/plane/app/urls/issue.py (1)
64-73
: LGTM! Well-structured API endpoints
The new URL patterns for description management are well-designed:
- Follow RESTful conventions
- Maintain consistent structure with other endpoints
- Properly scope resources by workspace/project/issue
Also applies to: 246-254
web/core/store/issue/issue-details/issue.store.ts (2)
61-68
: LGTM! Clean store initialization
The constructor changes properly initialize both root and detail stores, maintaining clean separation of concerns.
168-168
: LGTM! Proper payload update
The issue payload correctly includes the binary description field.
packages/editor/src/core/hooks/use-editor.ts (3)
49-49
: LGTM: New provider document prop enhances document management
The addition of providerDocument
as an optional Y.Doc type provides better control over document handling.
Line range hint 300-304
: LGTM: Consistent document update handling
The setProviderDocument
method correctly uses the new providerDocument
prop, maintaining consistency with the new document management approach.
Line range hint 222-229
: Verify document binary generation
The change to use providerDocument
for binary generation looks correct, but we should ensure all consumers are updated accordingly.
✅ Verification successful
The change to use providerDocument
for binary generation is consistent across the codebase
The verification shows that the binary generation pattern using Y.encodeStateAsUpdate(providerDocument)
is consistently implemented across the codebase:
- In
use-editor.ts
anduse-read-only-editor.ts
hooks - All collaborative editor hooks correctly pass
provider.document
asproviderDocument
- The binary format is properly consumed by the API and store layers through
description_binary
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for any remaining uses of provider?.document that might need updating
rg -A 2 'provider\?\.document'
Length of output: 32
Script:
#!/bin/bash
# Let's search for any usage of Y.encodeStateAsUpdate to understand the binary generation pattern
rg -A 2 'Y\.encodeStateAsUpdate'
# Also search for providerDocument to see its usage
rg -A 2 'providerDocument'
# Search for any document binary related patterns
rg -A 2 'documentBinary|binary:'
Length of output: 9255
web/core/services/issue/issue.service.ts (1)
392-405
: LGTM: Well-implemented binary description update
The updateDescriptionBinary
method is well-structured with:
- Proper typing using
Pick<TDocumentPayload, "description_binary">
- Correct response type handling with
ArrayBuffer
- Consistent error handling pattern
web/core/components/issues/peek-overview/root.tsx (2)
96-105
: LGTM: Robust error handling in description update
The implementation includes:
- Validation of required fields
- Clear error messages
- Proper error propagation
332-342
: LGTM: Comprehensive dependency array update
The addition of updateIssueDescription
to the dependency array ensures proper memoization of the issueOperations
object.
apiserver/plane/app/views/issue/archive.py (1)
299-323
: LGTM! The implementation follows best practices.
The retrieve_description
method is well-implemented with:
- Proper permission handling for different user roles
- Efficient streaming response for binary data
- Appropriate error handling
- Secure issue validation
However, consider adding content type validation and size limits for the binary data.
web/core/components/issues/issue-detail/root.tsx (2)
29-34
: LGTM! The type definition is well-structured.
The updateDescription
method signature is properly typed with all necessary parameters.
334-334
: LGTM! Dependencies are properly updated.
The updateIssueDescription
dependency is correctly added to the useMemo dependencies array.
web/core/store/issue/issue-details/root.store.ts (2)
195-195
: LGTM! Constructor initialization is correct.
The IssueStore is properly initialized with both rootStore and this instance.
260-265
: LGTM! Method implementation follows the established pattern.
The updateIssueDescription
method correctly delegates to the issue store's implementation.
apiserver/plane/app/serializers/issue.py (1)
631-656
: Verify integration points and test coverage.
Let's ensure the new binary field implementation is properly integrated:
✅ Verification successful
Let me gather more information about the integration points since the previous command had issues with Python file type recognition.
Binary field implementation is properly integrated across the codebase
The description_binary
field is well integrated throughout the codebase with:
- Proper model definition in
Issue
model with corresponding migrations (0066, 0070) - Consistent usage in views with proper binary data handling (base64 encoding/decoding)
- Integration in multiple serializers (IssueDetailSerializer, DraftIssueSerializer)
- Implementation in related features (drafts, archives, intake)
- Proper streaming support for binary data in views
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check integration points and test coverage for binary description
# Check views using IssueDetailSerializer
echo "Checking views using IssueDetailSerializer..."
rg -l "IssueDetailSerializer" --type python
# Check for related tests
echo "Checking test coverage..."
rg -l "description_binary" --type python --glob "*test*.py"
# Check for migrations
echo "Checking for related migrations..."
fd -e py -p ".*migrations.*" | xargs rg "description"
Length of output: 8451
Script:
#!/bin/bash
# Check views and tests with proper file type
echo "Checking views using IssueDetailSerializer..."
rg -t py "IssueDetailSerializer" -l
echo "\nChecking usage of description_binary..."
rg -t py "description_binary" -A 3
echo "\nChecking model definition..."
rg -t py "class Issue\(" -A 10
Length of output: 15353
this.document.on("update", this.documentUpdateHandler.bind(this)); | ||
this.document.on("destroy", this.documentDestroyHandler.bind(this)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix event listener removal to prevent memory leaks
The event listeners added in the constructor using bound functions are not being removed correctly in documentDestroyHandler
. Since bind
creates new function references, the handlers passed to off
are not the same as those passed to on
. This can lead to memory leaks.
Apply this diff to fix the issue:
export class CustomCollaborationProvider {
+ private boundDocumentUpdateHandler: (update: Uint8Array, origin: any) => void;
+ private boundDocumentDestroyHandler: () => void;
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));
+ this.boundDocumentUpdateHandler = this.documentUpdateHandler.bind(this);
+ this.boundDocumentDestroyHandler = this.documentDestroyHandler.bind(this);
+ this.document.on("update", this.boundDocumentUpdateHandler);
+ this.document.on("destroy", this.boundDocumentDestroyHandler);
}
documentDestroyHandler() {
- this.document.off("update", this.documentUpdateHandler);
- this.document.off("destroy", this.documentDestroyHandler);
+ this.document.off("update", this.boundDocumentUpdateHandler);
+ this.document.off("destroy", this.boundDocumentDestroyHandler);
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
this.document.on("update", this.documentUpdateHandler.bind(this)); | |
this.document.on("destroy", this.documentDestroyHandler.bind(this)); | |
} | |
export class CustomCollaborationProvider { | |
private boundDocumentUpdateHandler: (update: Uint8Array, origin: any) => void; | |
private boundDocumentDestroyHandler: () => void; | |
constructor(configuration: CollaborationProviderConfiguration) { | |
this.hasSynced = false; | |
this.setConfiguration(configuration); | |
this.boundDocumentUpdateHandler = this.documentUpdateHandler.bind(this); | |
this.boundDocumentDestroyHandler = this.documentDestroyHandler.bind(this); | |
this.document.on("update", this.boundDocumentUpdateHandler); | |
this.document.on("destroy", this.boundDocumentDestroyHandler); | |
} | |
documentDestroyHandler() { | |
this.document.off("update", this.boundDocumentUpdateHandler); | |
this.document.off("destroy", this.boundDocumentDestroyHandler); | |
} | |
} |
const provider = useMemo( | ||
() => | ||
new CustomCollaborationProvider({ | ||
name: id, | ||
onChange, | ||
}), | ||
[id] | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include onChange
in the dependency array of useMemo
The useMemo
hook depends on both id
and onChange
but only includes id
in its dependency array. If onChange
changes, the provider
won't be updated accordingly. Include onChange
to ensure the provider
reflects the latest onChange
handler.
Apply this diff to fix the issue:
const provider = useMemo(
() =>
new CustomCollaborationProvider({
name: id,
onChange,
}),
- [id]
+ [id, onChange]
);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const provider = useMemo( | |
() => | |
new CustomCollaborationProvider({ | |
name: id, | |
onChange, | |
}), | |
[id] | |
); | |
const provider = useMemo( | |
() => | |
new CustomCollaborationProvider({ | |
name: id, | |
onChange, | |
}), | |
[id, onChange] | |
); |
useEffect(() => { | ||
if (provider.hasSynced) return; | ||
if (value && value.length > 0) { | ||
try { | ||
Y.applyUpdate(provider.document, value); | ||
provider.hasSynced = true; | ||
} catch (error) { | ||
console.error("Error applying binary updates to the description", error); | ||
} | ||
} | ||
}, [value, provider.document]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add provider
to the dependency array in useEffect
The useEffect
hook uses provider.hasSynced
and provider.document
but only includes value
and provider.document
in its dependency array. To ensure that the effect runs when the provider
changes, include provider
in the dependency array.
Apply this diff to fix the issue:
useEffect(() => {
if (provider.hasSynced) return;
if (value && value.length > 0) {
try {
Y.applyUpdate(provider.document, value);
provider.hasSynced = true;
} catch (error) {
console.error("Error applying binary updates to the description", error);
}
}
-}, [value, provider.document]);
+}, [value, provider]);
Committable suggestion skipped: line range outside the PR's diff.
export const convertBinaryDataToBase64String = (document: Uint8Array): string => | ||
Buffer.from(document).toString("base64"); | ||
|
||
/** | ||
* @description this function decodes base64 string to binary data | ||
* @param {string} document | ||
* @returns {ArrayBuffer} | ||
*/ | ||
export const convertBase64StringToBinaryData = (document: string): ArrayBuffer => Buffer.from(document, "base64"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace Buffer
with browser-compatible methods
Using Buffer
is not compatible with browser environments as it's a Node.js API. Use TextEncoder
, TextDecoder
, or other browser-compatible methods to handle base64 encoding and decoding.
Apply this diff to fix the issue:
-export const convertBinaryDataToBase64String = (document: Uint8Array): string =>
- Buffer.from(document).toString("base64");
+export const convertBinaryDataToBase64String = (document: Uint8Array): string => {
+ let binary = '';
+ document.forEach((byte) => (binary += String.fromCharCode(byte)));
+ return btoa(binary);
+};
-export const convertBase64StringToBinaryData = (document: string): ArrayBuffer => Buffer.from(document, "base64");
+export const convertBase64StringToBinaryData = (document: string): Uint8Array => {
+ const binaryString = atob(document);
+ const len = binaryString.length;
+ const bytes = new Uint8Array(len);
+ for (let i = 0; i < len; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes;
+};
Ensure all environments where this code runs support btoa
and atob
, or consider using TextEncoder
and TextDecoder
.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
export const convertBinaryDataToBase64String = (document: Uint8Array): string => | |
Buffer.from(document).toString("base64"); | |
/** | |
* @description this function decodes base64 string to binary data | |
* @param {string} document | |
* @returns {ArrayBuffer} | |
*/ | |
export const convertBase64StringToBinaryData = (document: string): ArrayBuffer => Buffer.from(document, "base64"); | |
export const convertBinaryDataToBase64String = (document: Uint8Array): string => { | |
let binary = ''; | |
document.forEach((byte) => (binary += String.fromCharCode(byte))); | |
return btoa(binary); | |
}; | |
/** | |
* @description this function decodes base64 string to binary data | |
* @param {string} document | |
* @returns {ArrayBuffer} | |
*/ | |
export const convertBase64StringToBinaryData = (document: string): Uint8Array => { | |
const binaryString = atob(document); | |
const len = binaryString.length; | |
const bytes = new Uint8Array(len); | |
for (let i = 0; i < len; i++) { | |
bytes[i] = binaryString.charCodeAt(i); | |
} | |
return bytes; | |
}; |
key: string; | ||
placeholder?: string | ((isFocused: boolean, value: string) => string); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using key
as a prop name
key
is a special prop in React used for identifying elements in lists. Using it as a regular prop can cause unexpected behavior. Rename the prop to avoid conflicts.
Apply this diff to fix the issue:
export type IssueDescriptionInputProps = {
containerClassName?: string;
descriptionBinary: string | null;
descriptionHTML: string;
disabled?: boolean;
issueId: string;
- key: string;
+ editorKey: string;
placeholder?: string | ((isFocused: boolean, value: string) => string);
projectId: string;
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
updateDescription: (data: string) => Promise<ArrayBuffer>;
workspaceSlug: string;
};
Update the destructuring and usage:
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
const {
containerClassName,
descriptionBinary: savedDescriptionBinary,
descriptionHTML,
disabled,
issueId,
placeholder,
projectId,
setIsSubmitting,
updateDescription,
workspaceSlug,
- key,
+ editorKey,
} = props;
And in the component usage:
{!disabled ? (
<CollaborativeRichTextEditor
- key={issueId}
+ key={editorKey}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
key: string; | |
placeholder?: string | ((isFocused: boolean, value: string) => string); | |
editorKey: string; | |
placeholder?: string | ((isFocused: boolean, value: string) => string); |
@@ -1,4 +1,4 @@ | |||
import { useEffect, useMemo, useState } from "react"; | |||
import { useEffect, useImperativeHandle, useMemo, useState } from "react"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codebase verification
Let me generate the final response based on the shell script results.
useImperativeHandle is used in other editor hooks, but not in use-collaborative-document-editor.ts
The useImperativeHandle
import appears to be unused in the current file. It's used in other editor hooks like use-read-only-editor.ts
, use-editor.ts
, and mentions-list.tsx
, but not in use-collaborative-document-editor.ts
. Consider removing this unused import.
🔗 Analysis chain
Verify useImperativeHandle usage
The useImperativeHandle import has been added, but its usage is not visible in the provided code.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check if useImperativeHandle is used in the codebase
# Expected: Find usage of useImperativeHandle
ast-grep --pattern 'useImperativeHandle($$$)'
Length of output: 21551
updateDescription: async (_workspaceSlug, _projectId, _issueId, descriptionBinary) => { | ||
try { | ||
return await inboxIssue.updateIssueDescription(descriptionBinary); | ||
} catch { | ||
throw new Error("Failed to update issue description"); | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enhance error handling in updateDescription
The error handling could be improved by:
- Including error details in the thrown error
- Adding error logging
- Using the toast notification system consistently
updateDescription: async (_workspaceSlug, _projectId, _issueId, descriptionBinary) => {
try {
return await inboxIssue.updateIssueDescription(descriptionBinary);
- } catch {
- throw new Error("Failed to update issue description");
+ } catch (error) {
+ console.error("Error updating issue description:", error);
+ setToast({
+ title: "Error!",
+ type: TOAST_TYPE.ERROR,
+ message: "Failed to update issue description",
+ });
+ throw error;
}
},
Committable suggestion skipped: line range outside the PR's diff.
@@ -265,7 +265,11 @@ class Meta: | |||
|
|||
class DraftIssueDetailSerializer(DraftIssueSerializer): | |||
description_html = serializers.CharField() | |||
description_binary = serializers.CharField() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using Base64BinaryField for consistency and validation
The description_binary
field is implemented as a plain CharField
which could lead to:
- Inconsistency with other serializers that use
Base64BinaryField
- Missing validation for base64-encoded data
- Potential security risks from unvalidated binary content
Consider using the same field type as other serializers:
- description_binary = serializers.CharField()
+ description_binary = Base64BinaryField()
Also applies to: 271-274
updateIssueDescription = async ( | ||
workspaceSlug: string, | ||
projectId: string, | ||
issueId: string, | ||
descriptionBinary: string | ||
): Promise<ArrayBuffer> => { | ||
const res = await this.issueService.updateDescriptionBinary(workspaceSlug, projectId, issueId, { | ||
description_binary: descriptionBinary, | ||
}); | ||
this.rootIssueStore.issues.updateIssue(issueId, { description_binary: descriptionBinary }); | ||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); | ||
return res; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add error handling for description updates
The updateIssueDescription
method should handle potential errors that might occur during the API call or store updates.
Consider adding error handling:
updateIssueDescription = async (
workspaceSlug: string,
projectId: string,
issueId: string,
descriptionBinary: string
): Promise<ArrayBuffer> => {
+ try {
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;
+ } catch (error) {
+ console.error("Failed to update issue description:", error);
+ throw error;
+ }
};
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
updateIssueDescription = async ( | |
workspaceSlug: string, | |
projectId: string, | |
issueId: string, | |
descriptionBinary: string | |
): Promise<ArrayBuffer> => { | |
const res = await this.issueService.updateDescriptionBinary(workspaceSlug, projectId, issueId, { | |
description_binary: descriptionBinary, | |
}); | |
this.rootIssueStore.issues.updateIssue(issueId, { description_binary: descriptionBinary }); | |
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); | |
return res; | |
}; | |
updateIssueDescription = async ( | |
workspaceSlug: string, | |
projectId: string, | |
issueId: string, | |
descriptionBinary: string | |
): Promise<ArrayBuffer> => { | |
try { | |
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; | |
} catch (error) { | |
console.error("Failed to update issue description:", error); | |
throw error; | |
} | |
}; |
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") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add validation for binary data size and content.
The Base64BinaryField
implementation needs additional safeguards:
- Maximum size validation to prevent memory issues
- Content type validation to ensure security
- Better error handling to distinguish between different error cases
Consider applying this enhancement:
class Base64BinaryField(serializers.CharField):
+ def __init__(self, *args, **kwargs):
+ self.max_size = kwargs.pop('max_size', 10 * 1024 * 1024) # 10MB default
+ super().__init__(*args, **kwargs)
+
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:
+ if not data:
+ return None
decoded = base64.b64decode(data)
+ if len(decoded) > self.max_size:
+ raise serializers.ValidationError(
+ f"Binary data exceeds maximum size of {self.max_size} bytes"
+ )
return decoded
except TypeError:
- raise serializers.ValidationError("Invalid base64-encoded data")
+ raise serializers.ValidationError("Data is not a valid string")
+ except ValueError:
+ raise serializers.ValidationError("Invalid base64 encoding")
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
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 Base64BinaryField(serializers.CharField): | |
def __init__(self, *args, **kwargs): | |
self.max_size = kwargs.pop('max_size', 10 * 1024 * 1024) # 10MB default | |
super().__init__(*args, **kwargs) | |
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: | |
if not data: | |
return None | |
decoded = base64.b64decode(data) | |
if len(decoded) > self.max_size: | |
raise serializers.ValidationError( | |
f"Binary data exceeds maximum size of {self.max_size} bytes" | |
) | |
return decoded | |
except TypeError: | |
raise serializers.ValidationError("Data is not a valid string") | |
except ValueError: | |
raise serializers.ValidationError("Invalid base64 encoding") |
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation
Chores