feat(files): let the user set a file's type from the header dropdown - #6373
feat(files): let the user set a file's type from the header dropdown#6373mzxchandra wants to merge 10 commits into
Conversation
The file-detail filename dropdown gains a Type submenu offering the text-editable types (nine document formats plus a nested Code group). Picking one swaps the file's extension and its stored contentType in a single write, leaving the bytes untouched, so a file created as untitled.md can become untitled.json and open in the right editor. Renaming previously never touched contentType, so name and type could silently diverge; the retype path keeps them in agreement and the server re-derives the pairing rather than trusting the client.
updateWorkspaceFileContent read the row before taking the FOR UPDATE lock, then wrote that read's contentType back inside the transaction. A save overlapping a type change therefore restored the pre-change type, leaving the file named .txt while still stored as text/markdown. A content write carries no opinion about the file's type unless the caller says so, so the column is now only written when a contentType is supplied. The live-doc markdown gate reads the committed row for the same reason.
Changing a collaborative markdown file's type unmounts its editor and mounts one that reads the file's durable bytes. The relay owns durability for that document and persists on a 5s debounce, so the read raced the write and returned the content from before the last edits. The client cannot close this itself: its save path is disabled for a collaborative doc by design, and isDirty is pinned false. Adds a FLUSH/FLUSH_COMPLETE round trip so the client can ask the relay to project the document now and wait for the answer. flushPersist grows a mode and returns an outcome: only a debounced flush may be coalesced away by the cross-task dedup window, because a deduped no-op acked as success would ship exactly the staleness this closes. The client wait is bounded well under the persist budget and a lapsed wait proceeds with the rename rather than blocking the user.
The published flush was bound to the provider's identity, so every socket churn republished it — and a churn ending on null left the file-detail header with nothing to call, silently degrading a retype back to a stale read. It now publishes once and resolves the provider at call time. Adds a log on both sides of the flush. Its outcome decides whether the caller may treat the durable bytes as current, and unchanged/skipped are both silent no-writes, so a stale read after a retype is otherwise indistinguishable from a rendering bug.
…tten A requested flush deliberately bypasses the cross-task dedup window, because a deduped no-op acked as success would reintroduce the staleness the flush exists to prevent. But room.edited is set on the first edit and never cleared, so every repeat still performed a full projection: a Yjs-to-markdown conversion, a fresh blob upload, and a delete of the previous key. A client emitting flush in a loop could drive that unbounded. Pairs a monotonic edit counter with the sequence the last successful persist covered, so a flush with nothing new to write acks unchanged instead. The sequence is captured before the projection and stored only on success, so an edit arriving mid-write stays pending and a conflict is never mistaken for a completed write.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Collaborative markdown edits are no longer lost when retyping: the client calls a new Correctness fixes: content saves no longer overwrite UI plumbing: Reviewed by Cursor Bugbot for commit 2c1518e. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThe PR lets users change a text file’s type from the file-header dropdown while keeping its extension and stored MIME type aligned. It also coordinates collaborative-document persistence before retyping and avoids stale
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains, and the previously reported relative-import violation is fixed at current HEAD.
|
| Filename | Overview |
|---|---|
| apps/sim/app/workspace/[workspaceId]/files/files.tsx | Orchestrates type selection, collaborative flush, metadata refresh, and the rename mutation. |
| apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts | Updates filename and content type together while preserving content metadata unless explicitly supplied. |
| apps/realtime/src/handlers/file-doc.ts | Adds requested flush handling with truthful outcomes and edit-sequence deduplication. |
| packages/realtime-protocol/src/file-doc.ts | Defines the shared FLUSH and FLUSH_COMPLETE wire contract. |
| apps/sim/lib/uploads/utils/text-file-types.ts | Centralizes selectable text-file types and filename-extension resolution. |
| apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/collaboration/use-file-doc-collaboration.ts | Publishes the collaboration flush callback and now satisfies the previously reported absolute-import convention. |
Sequence Diagram
sequenceDiagram
participant U as User
participant UI as File Header
participant RT as Realtime Service
participant API as Workspace Files API
participant DB as Database/Object Storage
U->>UI: Select new text file type
UI->>RT: FLUSH collaborative document
RT->>DB: Persist pending edits
RT-->>UI: FLUSH_COMPLETE
opt Persist rotated storage key
UI->>API: Refresh workspace files
API->>DB: Read current file metadata
API-->>UI: Fresh storage metadata
end
UI->>API: PATCH name + contentType
API->>API: Validate extension/MIME agreement
API->>DB: Update file metadata atomically
API-->>UI: Updated file
UI->>UI: Mount editor for new type
Reviews (4): Last reviewed commit: "fix(files): surface a failed pre-retype ..." | Re-trigger Greptile
The .gstack/ entry was created by local tooling during QA on this branch and got swept in by a broad stage. It is unrelated to the file-type work and the repo references .gstack nowhere, so it does not belong in this PR.
… live key A collaborative flush persists through a versioned object swap: it mints a new storage key and deletes the previous blob. The retype then swaps editors from the optimistic rename patch, so the newly mounted viewer read `key` off a record the flush had already invalidated - a 404, or pre-edit text from the content cache keyed on that dead key, until the rename's own invalidation landed. Awaits a list refetch between a confirmed `persisted` flush and the rename. `refetchType: 'all'`, because the caller awaits this for a usable key and the default `active` resolves immediately against an unobserved list. Also moves this file's two sibling imports onto the `@/` alias per the repo's absolute-import rule.
|
@cursor review |
…owing it `invalidateQueries` resolves whether or not the refetch succeeded, so the refresh reported success while leaving the dead storage key in the cache - the caller awaiting a usable key could not tell the two apart. The hook now rejects on a failed refetch. The retype logs and proceeds rather than aborting: the edits are already durable, the type change is explicit, and the rename's own invalidation refetches straight after, so the cost of a failed refresh is one stale first paint - the pre-fix behaviour - not a lost change.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 2c1518e. Configure here.
|
@waleedlatif1 this touches file realtime |
Summary
handleCreateFilewas hardcoded to markdown, and there was no way to say "actually this is a CSV" afterwards. The only escape hatch was Rename - which never touchedcontentType, sountitled.mdrenamed todata.csvstayedtext/markdownand kept opening in the rich markdown editor.Feature
Document ›/Code ›). Picking a type swaps the file's extension and its storedcontentTypein one write. Bytes are never touched, so it's a metadata edit, not a conversion - only text-editable types are offered, and the entry is absent entirely on a PDF or an image.apps/sim/lib/uploads/utils/text-file-types.ts: 9 document types plus 20 code types. Every MIME is taken from the existingEXTENSION_TO_MIMErather than invented. It's framework-free, so the contract, route, hook, and component all consume one list. Two invariant tests keep it honest - every entry must round-trip throughgetMimeTypeFromExtension, and every entry must resolve totext-editableinresolveFileCategory, so nobody can add a type the viewer refuses to open.contentType, allowlisted and cross-checked against the name's extension. The check derives extension → MIME (extension is the registry's unique key; several types share a MIME), and the server re-derives it rather than trusting the client.Correctness fixes found while building it
updateWorkspaceFileContentread the row before taking theFOR UPDATElock, then wrote that read'scontentTypeback inside it. A content save overlapping a retype therefore resurrected the pre-retype type, leaving a file named.txtstill stored astext/markdown. It now writes the column only when a caller actually declares one. This was latent before this PR too - a save racing a plain rename hit the same path.Code ›entry overflows the 240px menu cap, which pushed the entry gating every code type out of sight on open.Realtime flush (partially verified - see Verification Results)
isDirtyis pinnedfalse.FLUSH/FLUSH_COMPLETEround trip.flushPersistgrows a mode and returns an outcome: only adebouncedflush may be coalesced away by the cross-task dedup window, because a deduped no-op acked as success would ship exactly the staleness this closes. The client wait is bounded well under the persist budget, and a lapsed wait proceeds with the rename rather than blocking the user.nullsilently left the header with nothing to call.persistedflush and the rename (useRefreshWorkspaceFiles,refetchType: 'all'- the defaultactiveresolves instantly against an unobserved list). The refresh rejects on a failed refetch rather than reporting success on the stale cache; the caller logs and proceeds, since the edits are already durable and aborting would lose an explicit user action over a transient fetch.Test Coverage
Tests: 4 new test files, 6 extended.
apps/sim20,545 passed / 0 failed.apps/realtime283 passed / 0 failed.Pre-Landing Review
2 issues (1 critical, 1 informational).
apps/realtime/src/handlers/file-doc.ts- a client-requested flush was unbounded.requesteddeliberately bypasses the dedup window, butroom.editedis set on the first edit and never cleared, so every repeat performed a full projection: a Yjs→markdown conversion, a fresh blob upload, and a delete of the previous key. Fixed by pairing a monotonic edit counter with the sequence the last successful persist covered; a flush with nothing new to write now acksunchanged. The sequence is captured before the projection and stored only on success, so an edit arriving mid-write stays pending and a conflict is never mistaken for a completed write.file-doc-room-context.tsx-fileId: ''sentinel on the no-provider fallback. The only consumer branches onstatus, so it is harmless today.Specialist subagents were not dispatched; this is the checklist pass run inline.
Design Review
Frontend files changed. Reviewed inline against the emcn consumer rules: the Type entry is props-driven through the existing
BreadcrumbItem.dropdownItemscontract, the newDropdownSubmenuOptionis a discriminated sibling so every existing consumer compiles unchanged, and noclassNameoverrides chrome.DropdownMenuSubTriggersupplies its own chevron andDropdownMenuRadioItemreserves its own indicator gutter, so neither is re-specified.No full
/design-reviewvisual audit was run.Eval Results
No prompt-related files changed - evals skipped.
Scope Drift
contentTyperace fix is not optional - without it the relay's persist reverts the type you just picked.No unrelated files remain. A stray
.gitignoreline (.gstack/, a local tooling artifact from QA) was swept in by a broad stage and has been removed ine278ae38-.gitignorenow matchesstagingexactly.Plan Completion
12 DONE, 3 CHANGED, 1 PARTIAL, 0 NOT DONE.
The three CHANGED items: both type groups nested rather than one inline (the 240px menu cap); manager coverage landed in a new
workspace-file-retype.test.tsrather than extendingworkspace-file-manager.test.ts(no DB harness there); and the registry↔viewer invariant lives intext-file-types.test.tsrather thanfile-category.test.ts, which mocks the dependencies that invariant needs real.Verification Results
Browser QA against local dev - 8 of 11 cases verified:
untitled.md, rich editor, Type column "Markdown"Document ›/Code ›groupsuntitled.py(1)suffix, no 409content_typeagree in the database after every changeThe stale first paint had a root cause, and it is fixed. It was not only the flush budget: a confirmed
persistedflush rotates the storage key and deletes the old blob, so the viewer that mounted next read a dead key. Surfaced by Cursor in review and fixed ine1775025/2c1518ed(see the flush section above). Durability was already measurably improved before that - a fresh file's stored bytes went fromsize 0to the typed content as part of the retype - and the relay loggedRequested flush … persisted { edited: true }, so the round trip works end to end. The happy path has still not been re-run in a browser since the fix; the reviewer should treat that as the one open verification item./api/internal/file-doc/persistcosts 3.6s cold vs 8ms warm in dev, so in a dev session the first retype after a server start will reliably lapse that budget. Worth a second look at whether 2s is the right number.Also not verified: multi-collaborator retype, read-only member gating, and realtime propagation to a second session.
Notes for the reviewer
staging:apps/realtime/src/handlers/file-doc.test.tshas 3 failing seed assertions on a cleanorigin/stagingcheckout (expected '' to contain '# Seeded'). They pass on this branch. Worth a look from whoever owns that file - a test that flips based on what else is in the file is fragile.app/api/files/serve/[...path]/route.tsderives both from the storage key, which is frozen at upload and never rewritten byrenameWorkspaceFile; the client then lets thatContent-Dispositionwin over the DB record. Renaming already breaks this today. Retyping to a non-markdown type moves files onto that path, so it becomes more visible.apps/docs/content/docs/en/files/has aneditor.mdxthat covers the markdown editor. This feature likely warrants a mention there, but the docs carry a specific voice and screenshots, so that is left to the docs owner rather than done as a side effect.Test plan
apps/sim- 20,544 passed, 0 failed, 25 skippedapps/realtime- 283 passed, 0 failedpackages/realtime-protocol- 14 passedbun run check:api-validationpassesbunx tsc --noEmitclean inapps/simandapps/realtime🤖 Generated with Claude Code