Skip to content

Add board duplication to a new board (roomDup)#237

Open
rebeccanesson wants to merge 1 commit into
edemaine:mainfrom
rebeccanesson:rn/duplicate-board-to-new-board
Open

Add board duplication to a new board (roomDup)#237
rebeccanesson wants to merge 1 commit into
edemaine:mainfrom
rebeccanesson:rn/duplicate-board-to-new-board

Conversation

@rebeccanesson

Copy link
Copy Markdown

Proposal

This is a proposal — happy to adjust the API, naming, UX, or scope (or split it up) to fit how you'd like the feature to work. No worries if it's not a direction you want to take.

What & why

Cocreate can duplicate a page within a board (pageDup). This adds the ability to duplicate an entire board — all pages and all objects — into a brand new board with its own URL, leaving the original untouched.

Motivating use case: an instructor sets up a problem on a board and hands out a link; each student who clicks it gets their own independent copy to work on and export, without touching the original or each other's work.

Changes

  • lib/rooms.coffee — new roomDup(roomId) method that mirrors pageDup's structure: creates a new room, then copies every page (in order) and its objects, repointing room/page. Server-only (return if @isSimulation) so the caller just awaits the result — works even when the visitor hasn't subscribed to the source board.
  • client/CloneRoom.coffee + client/App.coffee — new /clone/:roomId route. Visiting it clones the board and redirects to the copy. This is the shareable "handout" link.
  • client/tools/room.coffee — a "duplicate board" button in the room toolbar (opens the copy in a new tab), plus a clone link surfaced in the existing "Link to this room/board" dialog.
  • CHANGELOG.md — user-facing entry.

Testing

  • npm run lint passes (eslint + markdownlint).
  • Ran a local meteor server and exercised roomDup end-to-end: with a multi-page board containing objects, the copy has fresh page IDs, all objects copied and repointed to the new room/pages, page order preserved, and the original board unchanged. Also smoke-tested the toolbar button, the /clone/:roomId link, and the dialog clone link in the browser.

Open questions

  • Route shape (/clone/:roomId) and method name (roomDup) are my best guess at fitting existing conventions — easy to change.
  • The clone link in the share dialog is an addition to an already-busy modal; happy to drop it if you'd prefer to keep the dialog lean and rely on the toolbar button + route alone.

Add a roomDup server method that copies a board's pages and objects
into a brand new room, leaving the original untouched.

Exposed three ways:
- a "duplicate board" button in the room toolbar (opens the copy in a
  new tab);
- a /clone/:roomId link that clones and redirects, so each visitor gets
  their own independent copy;
- a clone link surfaced in the "Link to this room/board" dialog.

Motivating use case: hand out a link to a prepared problem board so each
student works on their own copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds board duplication to Cocreate: a new roomDup Meteor method copies all pages and objects into a fresh room, a /clone/:roomId route lets visitors get their own independent copy via a shareable link, and a toolbar button duplicates the current board into a new tab.

  • lib/rooms.coffee: roomDup mirrors pageDup's structure — iterates room.pages in order, calls pageNew per page, then bulk-inserts objects with updated room/page references. Server-only (return if @isSimulation), so the new room ID is reliably returned to the caller.
  • client/CloneRoom.coffee + client/App.coffee: New /clone/:roomId route clones the board and replaces the history entry with the new room URL; error case is surfaced in a modal.
  • client/tools/room.coffee: Clone link added to the share dialog; new dupRoom toolbar button opens the duplicate in a new tab.

Confidence Score: 4/5

The core duplication logic is sound and consistent with the existing pageDup pattern; the main gaps are UX polish rather than correctness.

The roomDup method correctly copies pages in order and repoints all object references to the new room and page IDs, matching the established pageDup pattern. The /clone/:roomId route and toolbar button wire it up correctly. The main concerns are: no loading indicator in CloneRoom (blank page during clone), no user-visible error feedback from the dupRoom button, no double-click guard on the toolbar button, and the relatively high per-call write cost of roomDup with no rate limiting — all addressable without changing the core design.

client/CloneRoom.coffee and the dupRoom handler in client/tools/room.coffee would benefit from loading/error state improvements before the feature ships to end users.

Important Files Changed

Filename Overview
lib/rooms.coffee Adds roomDup Meteor method that copies all pages and objects to a new room; logic mirrors pageDup closely and looks correct, but has no rate-limiting guard for an operation with high per-call DB write cost.
client/CloneRoom.coffee New route component that calls roomDup and redirects; functional and handles errors, but renders nothing while the clone is in progress, leaving visitors with a blank page for potentially several seconds.
client/tools/room.coffee Adds clone-link display to the share dialog and a new dupRoom toolbar button; both work but lack in-flight guards against double-invocation and user-visible error feedback.
client/App.coffee Registers the new /clone/:roomId route with the existing router; trivial and correct.
CHANGELOG.md User-facing changelog entry for the board duplication feature; accurate and well-written.

Sequence Diagram

sequenceDiagram
    participant User
    participant Browser
    participant CloneRoute as /clone/:roomId
    participant Server as Meteor Server
    participant DB as MongoDB

    User->>Browser: Visit /clone/:roomId
    Browser->>CloneRoute: Render CloneRoom component
    CloneRoute->>Server: Meteor.call('roomDup', roomId)
    Server->>DB: Rooms.findOne(roomId)
    DB-->>Server: room document
    Server->>DB: Rooms.insert(newRoom)
    DB-->>Server: newRoomId
    loop for each page in room.pages
        Server->>DB: Pages.findOne(pageId)
        Server->>Server: pageNew(page) → newPageId
        Server->>DB: Rooms.update $push newPageId
        loop for each object on page
            Server->>DB: "Objects.find({room, page})"
            Server->>Server: objectNew(obj) with new room/page
            Server->>DB: Objects.insert(obj)
        end
    end
    Server-->>CloneRoute: newRoomId
    CloneRoute->>Browser: navigate('/r/newRoomId', replace:true)
    Browser-->>User: New board opened

    Note over User,Browser: Toolbar button (dupRoom)
    User->>Browser: Click Duplicate Board
    Browser->>Server: Meteor.call('roomDup', currentRoom().id)
    Server-->>Browser: newRoomId
    Browser->>User: window.open('/r/newRoomId')
Loading

Reviews (1): Last reviewed commit: "Add board duplication to a new board (ro..." | Re-trigger Greptile

Comment thread client/CloneRoom.coffee
Comment on lines +19 to +27
### don't render anything while redirecting ###
<Show when={error()}>
<div class="modal error">
<h1>Failed to Clone Board</h1>
<p>Perhaps the board link is wrong, or you're disconnected from the network?</p>
<pre>{error().toString()}</pre>
<p><a href={Meteor.absoluteUrl()}>Create a new blank board instead</a></p>
</div>
</Show>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No loading state while clone is in progress

The component renders nothing (<Show when={error()}> is the only output) until either an error occurs or navigation fires. For a board with many pages and objects, roomDup can take several seconds; during that time the visitor sees a completely blank page with no spinner or message. They may assume the link is broken and refresh, which would trigger a second clone.

Consider rendering a brief "Cloning board…" placeholder outside the <Show> block so the user knows something is happening.

Comment thread client/tools/room.coffee
Comment on lines +85 to +89
click: ->
Meteor.call 'roomDup', currentRoom().id, (error, roomId) ->
if error?
return console.error "Failed to duplicate room on server: #{error}"
window.open "/r/#{roomId}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 dupRoom errors are only logged to the console, not shown to the user

If roomDup fails (e.g., the server throws "Invalid room ID"), console.error is called but nothing appears in the UI. The user clicks the button, nothing opens, and has no idea whether a duplicate was created or why it failed. A brief alert or modal message on error would make the failure visible.

Comment thread client/tools/room.coffee
Comment on lines +85 to +89
click: ->
Meteor.call 'roomDup', currentRoom().id, (error, roomId) ->
if error?
return console.error "Failed to duplicate room on server: #{error}"
window.open "/r/#{roomId}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 dupRoom button has no in-flight guard against double-invocation

Each click fires a fresh Meteor.call 'roomDup'; rapid double-clicking (or impatient users) would create multiple duplicate rooms and open several tabs. There is no visual loading state and the button stays active during the async call. Disabling the button (or tracking a loading signal) while the call is in flight would prevent this.

Comment thread lib/rooms.coffee
Comment on lines +39 to +66
roomDup: (roomId) ->
check roomId, String
return if @isSimulation
room = checkRoom roomId
sourcePages = room.pages ? []
delete room._id
delete room.created
room.created = new Date
room.pages = []
newRoomId = Rooms.insert room
for pageId in sourcePages
page = Pages.findOne pageId
continue unless page?
delete page._id
delete page.created
page.room = newRoomId
newPageId = Meteor.apply 'pageNew', [page]
Objects.find
room: roomId
page: pageId
.forEach (obj) ->
delete obj._id
delete obj.created
delete obj.updated
obj.room = newRoomId
obj.page = newPageId
Meteor.call 'objectNew', obj
newRoomId

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 roomDup is publicly callable with no rate limiting

Any visitor (anonymous or not) who knows — or guesses — a room ID can call roomDup to create arbitrarily many new rooms, each populated with every object from the source board. A tightly-looped script could flood the rooms, pages, and objects collections quickly. None of the other methods in this file have rate limiting either, so this is consistent with the existing security model, but the cost-per-call is significantly higher here than for a typical single-document write. Worth considering whether a per-IP or per-connection call-rate limit (DDPRateLimiter) is appropriate for this method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant