Open
Conversation
Reviewer's GuideImplements robust, deep duplication behavior for pages, database blocks, and rows (including row sub-documents) while tightening sync semantics and wiring the new duplication APIs through editor, database, and app contexts; also cleans up some Playwright helpers and auth utilities. Sequence diagram for page duplicate via MoreActions menusequenceDiagram
actor User
participant RightMenu
participant MoreActionsContent
participant SyncInternalContext as SyncInternal
participant useBatchSync as BatchSync
participant CollabAPI as collabFullSyncBatch
participant PageService
participant PageAPI as page_api_duplicatePage
participant ViewService
participant Outline as OutlineCache
User->>RightMenu: click_more_actions(viewId)
RightMenu->>MoreActionsContent: render(viewId)
User->>MoreActionsContent: click_duplicate()
MoreActionsContent->>MoreActionsContent: itemClicked()
MoreActionsContent->>MoreActionsContent: showBlockingLoader()
rect rgb(230,230,250)
MoreActionsContent->>SyncInternal: syncAllToServer(workspaceId)
SyncInternal->>BatchSync: syncAllToServer(workspaceId)
BatchSync->>BatchSync: flushAllSync()
BatchSync->>BatchSync: collect_registered_collabs()
BatchSync->>BatchSync: load_unregistered_row_docs()
BatchSync->>BatchSync: load_unregistered_row_subdocs()
BatchSync->>CollabAPI: collabFullSyncBatch(workspaceId,items)
CollabAPI-->>BatchSync: ok_or_error
BatchSync-->>SyncInternal: resolved(ignore_errors)
end
MoreActionsContent->>PageService: duplicate(workspaceId,viewId,options)
PageService->>PageAPI: duplicatePage(workspaceId,viewId,options)
PageAPI->>PageAPI: build_payload(parent_view_id,include_children,suffix,source,open_after_duplicate)
PageAPI->>PageAPI: POST /page-view/{viewId}/duplicate
PageAPI-->>PageService: success
PageService-->>MoreActionsContent: success
MoreActionsContent->>OutlineCache: refreshOutline(workspaceId)
OutlineCache-->>MoreActionsContent: updated_outline
MoreActionsContent->>ViewService: invalidateCache(workspaceId,parentViewId)
MoreActionsContent->>ViewService: loadViewChildren(parentViewId)
ViewService-->>MoreActionsContent: children_with_duplicate
MoreActionsContent->>MoreActionsContent: hideBlockingLoader()
MoreActionsContent-->>User: duplicate_complete
Sequence diagram for inline database block duplicate from ControlsMenusequenceDiagram
actor User
participant ControlsMenu
participant Editor as YjsEditor
participant CustomEditor
participant duplicateDatabaseBlock
participant ViewService
participant ViewAPI as getView
participant PageOps as duplicatePage
participant DBViewAPI as createDatabaseView
User->>ControlsMenu: open_controls_for_block()
ControlsMenu->>ControlsMenu: compute_options(selectedBlockIds)
User->>ControlsMenu: click_duplicate_blocks()
ControlsMenu->>ControlsMenu: duplicateSelectedBlocks()
loop for_each_selected_block
ControlsMenu->>Editor: findSlateEntryByBlockId(blockId)
Editor-->>ControlsMenu: [node,path]
ControlsMenu->>CustomEditor: duplicateBlock(editor,blockId,prevSiblingId)
CustomEditor-->>ControlsMenu: duplicatedBlockId
alt node_is_database_block
ControlsMenu->>duplicateDatabaseBlock: duplicateDatabaseBlock(sourceNode,duplicatedBlockId)
duplicateDatabaseBlock->>duplicateDatabaseBlock: parse_source_metadata(parentId,viewIds,databaseId)
alt linked_database_block
duplicateDatabaseBlock->>DBViewAPI: createDatabaseView(parentId,payload) * view_count
DBViewAPI-->>duplicateDatabaseBlock: view_id_list
duplicateDatabaseBlock->>duplicateDatabaseBlock: replaceDuplicatedBlockData(new_view_ids,existing_database_id)
else inline_database_block
duplicateDatabaseBlock->>ViewService: invalidateCache(workspaceId,sourceContainerId)
duplicateDatabaseBlock->>ViewService: invalidateCache(workspaceId,parentId)
duplicateDatabaseBlock->>ViewAPI: getView(workspaceId,sourceContainerId,2)
ViewAPI-->>duplicateDatabaseBlock: sourceContainerView
duplicateDatabaseBlock->>ViewAPI: getView(workspaceId,parentId,2) as beforeParentView
ViewAPI-->>duplicateDatabaseBlock: beforeParentView
duplicateDatabaseBlock->>PageOps: duplicatePage(sourceContainerId,options)
PageOps-->>duplicateDatabaseBlock: success
loop poll_for_duplicated_container(max_attempts)
duplicateDatabaseBlock->>ViewService: invalidateCache(workspaceId,parentId)
duplicateDatabaseBlock->>ViewAPI: getViewNoCache(workspaceId,parentId,2)
ViewAPI-->>duplicateDatabaseBlock: afterParentView
duplicateDatabaseBlock->>duplicateDatabaseBlock: findDuplicatedContainerChild(beforeChildren,afterChildren)
end
duplicateDatabaseBlock->>ViewAPI: getViewNoCache(workspaceId,duplicatedContainerId,2)
ViewAPI-->>duplicateDatabaseBlock: duplicatedContainer
duplicateDatabaseBlock->>duplicateDatabaseBlock: compute_new_view_ids_and_database_id()
duplicateDatabaseBlock->>duplicateDatabaseBlock: replaceDuplicatedBlockData(new_view_ids,new_database_id)
end
end
end
ControlsMenu->>Editor: ReactEditor.focus(editor)
ControlsMenu->>Editor: select(start_of_first_duplicated_block)
ControlsMenu-->>User: duplicate_done
alt any_database_block_duplicated
ControlsMenu->>ControlsMenu: notify.success(duplicateSuccessfully)
end
Sequence diagram for duplicate row with sub-documentsequenceDiagram
actor User
participant DatabaseGrid
participant useDuplicateRowDispatch as DuplicateRowDispatch
participant DatabaseContext
participant Cache as RowSubDocCache
participant DBOps as useDatabaseOperations
participant CollabAPI as duplicateRowDocumentAPI
User->>DatabaseGrid: duplicate_row(referenceRowId)
DatabaseGrid->>DuplicateRowDispatch: duplicateRow(referenceRowId)
DuplicateRowDispatch->>DuplicateRowDispatch: lookup_reference_row(rowMap)
DuplicateRowDispatch->>DuplicateRowDispatch: read_reference_meta(icon,cover,documentId,isEmptyDocument)
DuplicateRowDispatch->>DuplicateRowDispatch: generateRowMeta(new_row_id,IsDocumentEmpty_false,icon,cover)
DuplicateRowDispatch->>DatabaseContext: createRow(rowKey)
DatabaseContext-->>DuplicateRowDispatch: rowDoc
DuplicateRowDispatch->>DuplicateRowDispatch: initialDatabaseRow(new_row_id,databaseId,rowDoc)
DuplicateRowDispatch->>DuplicateRowDispatch: clone_cells_from_reference_row()
DuplicateRowDispatch->>DuplicateRowDispatch: write_meta(newMeta)
DuplicateRowDispatch->>DuplicateRowDispatch: insert_row_order_after_reference()
alt DatabaseContext_has_duplicateRowDocument
DuplicateRowDispatch->>DatabaseContext: duplicateRowDocument(databaseId,referenceRowId,new_row_id,clientDocStateB64?)
activate DatabaseContext
alt reference_has_documentId
DatabaseContext->>Cache: getCachedRowSubDoc(documentId)
Cache-->>DatabaseContext: cachedDoc_or_undefined
alt cachedDoc_exists
DatabaseContext->>DatabaseContext: encode_state_as_update(cachedDoc)
DatabaseContext->>DatabaseContext: base64_encode_state_chunked()
DatabaseContext->>DBOps: duplicateRowDocument(databaseId,referenceRowId,new_row_id,clientDocStateB64)
else no_cachedDoc
DatabaseContext->>DBOps: duplicateRowDocument(databaseId,referenceRowId,new_row_id,undefined)
end
else no_reference_documentId
DatabaseContext->>DBOps: duplicateRowDocument(databaseId,referenceRowId,new_row_id,undefined)
end
deactivate DatabaseContext
DBOps->>CollabAPI: duplicateRowDocument(workspaceId,databaseId,sourceRowId,newRowId,clientDocStateB64)
CollabAPI-->>DBOps: void
else no_duplicateRowDocument
DuplicateRowDispatch->>DuplicateRowDispatch: skip_server_document_duplicate()
end
DuplicateRowDispatch-->>DatabaseGrid: return new_row_id
DatabaseGrid-->>User: row_and_document_duplicated
Updated class diagram for duplication-related types and contextsclassDiagram
class DuplicatePageOptions {
+string parentViewId
+boolean openAfterDuplicate
+boolean includeChildren
+string suffix
+number source
}
class ViewComponentProps {
+function createRowDocument(documentId:string) Promise~Uint8Array|
+function duplicateRowDocument(databaseId:string,sourceRowId:string,newRowId:string,clientDocStateB64:string) Promise~void|
+ViewMetaProps viewMeta
+function appendBreadcrumb(breadcrumb:AppendBreadcrumb) void
+function onRendered() void
+function updatePage(viewId:string,data:UpdatePagePayload) Promise~void|
+function addPage(parentId:string,payload:CreatePagePayload) Promise~CreatePageResponse|
+function deletePage(viewId:string) Promise~void|
+function duplicatePage(viewId:string,options:DuplicatePageOptions) Promise~void|
+function openPageModal(viewId:string) void
}
class AppOperationsContextType {
+function addPage(parentId:string,payload:CreatePagePayload) Promise~CreatePageResponse|
+function openPageModal(viewId:string) void
+function deletePage(viewId:string) Promise~void|
+function duplicatePage(viewId:string,options:DuplicatePageOptions) Promise~void|
+function updatePage(viewId:string,payload:UpdatePagePayload) Promise~void|
+function updatePageIcon(workspaceId:string,viewId:string,icon:unknown) Promise~void|
+function updatePageName(viewId:string,name:string) Promise~void|
+function checkIfRowDocumentExists(documentId:string) Promise~boolean|
+function loadRowDocument(documentId:string) Promise~YDoc|
+function createRowDocument(documentId:string) Promise~Uint8Array|
+function duplicateRowDocument(databaseId:string,sourceRowId:string,newRowId:string,clientDocStateB64:string) Promise~void|
}
class EditorContextState {
+string workspaceId
+function addPage(parentId:string,payload:CreatePagePayload) Promise~CreatePageResponse|
+function deletePage(viewId:string) Promise~void|
+function duplicatePage(viewId:string,options:DuplicatePageOptions) Promise~void|
+function openPageModal(viewId:string) void
+function createDatabaseView(viewId:string,payload:CreateDatabaseViewPayload) Promise~CreateDatabaseViewResponse|
+function navigateToView(viewId:string,blockId:string) Promise~void|
+function createRow(rowKey:string) Promise~YDoc|
+function generateAISummaryForRow(payload:GenerateAISummaryRowPayload) Promise~string|
}
class DatabaseContextState {
+string workspaceId
+function createRow(rowKey:string) Promise~YDoc|
+function checkIfRowDocumentExists(documentId:string) Promise~boolean|
+function loadRowDocument(documentId:string) Promise~YDoc|
+function createRowDocument(documentId:string) Promise~Uint8Array|
+function duplicateRowDocument(databaseId:string,sourceRowId:string,newRowId:string,clientDocStateB64:string) Promise~void|
+function navigateToView(viewId:string,blockId:string) Promise~void|
+function createDatabaseView(viewId:string,payload:CreateDatabaseViewPayload) Promise~CreateDatabaseViewResponse|
+function updatePage(viewId:string,payload:UpdatePagePayload) Promise~void|
+function addPage(parentId:string,payload:CreatePagePayload) Promise~CreatePageResponse|
+function openPageModal(viewId:string) void
}
class Database2Props {
+string workspaceId
+function createRow(rowKey:string) Promise~YDoc|
+function checkIfRowDocumentExists(documentId:string) Promise~boolean|
+function loadRowDocument(documentId:string) Promise~YDoc|
+function createRowDocument(documentId:string) Promise~Uint8Array|
+function duplicateRowDocument(databaseId:string,sourceRowId:string,newRowId:string,clientDocStateB64:string) Promise~void|
+function navigateToView(viewId:string,blockId:string) Promise~void|
+function addPage(parentId:string,payload:CreatePagePayload) Promise~CreatePageResponse|
+function openPageModal(viewId:string) void
+function updatePage(viewId:string,payload:UpdatePagePayload) Promise~void|
+function deletePage(viewId:string) Promise~void|
}
class PageAPI {
+function duplicatePage(workspaceId:string,viewId:string,options:DuplicatePageOptions) Promise~void|
}
ViewComponentProps ..> DuplicatePageOptions
AppOperationsContextType ..> DuplicatePageOptions
EditorContextState ..> DuplicatePageOptions
DatabaseContextState ..> DuplicatePageOptions
Database2Props ..> DuplicatePageOptions
PageAPI ..> DuplicatePageOptions
EditorContextState ..> AppOperationsContextType : created_from
DatabaseContextState ..> AppOperationsContextType : uses
Database2Props ..> DatabaseContextState : provided_to
ViewComponentProps ..> AppOperationsContextType : consumes
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The new database/page duplication logic introduces multiple hard-coded retry/timeout values (e.g. polling loops in
duplicateDatabaseBlock,openRowSubDocument, and the 8s pre-sync timeout); consider centralizing these constants or using a shared helper to make tuning and reasoning about retry behavior easier and avoid subtle inconsistencies. - The inline duplication flow in
ControlsMenuanduseDuplicateRowDispatchnow performs multiple async steps (HTTP calls, polling, base64 encoding large Yjs updates) directly in UI handlers; extracting some of this into reusable utilities (e.g. aduplicateRowDocumentWithStatehelper and a genericpollForNewChildView) would simplify the components and reduce the risk of subtle bugs in the orchestration logic. - There are now several places constructing the duplicate copy suffix via
t('menuAppHeader.pageNameSuffix')(e.g.MoreActionsContent,ControlsMenu); consider extracting a small helper to compute this suffix so behavior stays consistent across different duplication entry points and is easier to adjust in the future.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new database/page duplication logic introduces multiple hard-coded retry/timeout values (e.g. polling loops in `duplicateDatabaseBlock`, `openRowSubDocument`, and the 8s pre-sync timeout); consider centralizing these constants or using a shared helper to make tuning and reasoning about retry behavior easier and avoid subtle inconsistencies.
- The inline duplication flow in `ControlsMenu` and `useDuplicateRowDispatch` now performs multiple async steps (HTTP calls, polling, base64 encoding large Yjs updates) directly in UI handlers; extracting some of this into reusable utilities (e.g. a `duplicateRowDocumentWithState` helper and a generic `pollForNewChildView`) would simplify the components and reduce the risk of subtle bugs in the orchestration logic.
- There are now several places constructing the duplicate copy suffix via `t('menuAppHeader.pageNameSuffix')` (e.g. `MoreActionsContent`, `ControlsMenu`); consider extracting a small helper to compute this suffix so behavior stays consistent across different duplication entry points and is easier to adjust in the future.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- Fix closeRowDetailWithEscape: defocus editor before Escape, use backdrop click as fallback instead of clicking the expand button (which navigated to full page instead of closing) - Fix typeInRowDocument: scroll to editor, use data-testid selector, use keyboard.type instead of pressSequentially for Slate compat - Fix row-document focus test: add Yjs sync wait and scroll on reopen - Fix row-document indicator test: defocus editor before close - Fix duplicate-row-doc-content test: use full-page editor for typing, reload-based retry for server-side async document duplication - Memoize duplicateCopySuffix in MoreActionsContent and ControlsMenu to prevent unnecessary useCallback recreation (react best practice) - Fix duplicate view-id mapping to use index-based matching Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Checklist
General
Testing
Feature-Specific
Summary by Sourcery
Improve page, block, and database duplication to avoid stale or empty content and better integrate with server-side duplication flows.
New Features:
Bug Fixes:
Enhancements:
Tests: