feat(documents): rename a document folder in place - #69
Open
Adron wants to merge 1 commit into
Open
Conversation
Renaming a folder previously meant deleting and recreating it, which took the folder's documents and subfolders with it. Closes #55. - Add `APIClient.updateDocumentFolder(id:name:parentId:)` hitting `PUT /api/documents/folders/{id}` via `putCamel` — the route reads plain camelCase keys, so the snake_case `put` would send `parent_id` and the server would silently ignore the move. Lives in a new `APIClient+Documents.swift` extension per CLAUDE.md rather than growing `APIClient.swift`. - Model `parentId` as `DocumentFolderParentUpdate` (`.unchanged` / `.root` / `.folder`) with a hand-written `encode(to:)`. The route validates and moves whenever the key is *present*, and Swift's synthesized `Encodable` drops a `nil` optional — so a rename must omit the key entirely, while a move to root needs an explicit `null`. One signature can't express that with a plain `String?`. - Wire Rename into the folder row's context menu and swipe actions in both `DocumentsView` and its nested `DocumentFolderView`, as an inline text field swapped in for the row. The write is optimistic and rolls the row back to the untouched original when the server refuses it. - Surface the server's own copy on refusal: the `400` circular-move rejection and the `409` duplicate-name conflict both arrive as `APIError.server(_:)` with text that reads well as-is. - Mirror the result into `AppDataStore.updateDocumentFolder(_:)` so the rename survives a cold start. Deliberately enqueues no sync operation, unlike its `createDocumentFolderOffline`/`deleteDocumentFolderOffline` neighbours: `POST /api/documents/sync` answers a folder update with `unsupported_operation` ("Folders support only `create` and `delete`"), so an outbox op would be rejected on every push and the rename would never persist. - 13 request-shape tests covering the verb and path, the omitted vs. explicit `parentId`, camelCase keys, the absence of a `folderId` query (folder routes are path-scoped), and the 400/409/401/no-data paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P1SLk6x1krwZ5FgPWGTHR5
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.
Summary
APIClientcould create and delete document folders but not update one, so fixing a typo in afolder name meant deleting the folder and recreating it — which took its documents and subfolders
with it. This adds the missing
PUT /api/documents/folders/{id}call and a Rename affordance inthe Documents UI, reaching parity with the web's
RenameFolderHeader/RenameFolderInput.Closes #55.
What's included
All in commit 7f79eb0.
Services/APIClient+Documents.swift(new) —updateDocumentFolder(id:name:parentId:)onPUT /api/documents/folders/{id}viaputCamel: the route reads plain camelCase keys, sothe snake_case
putwould sendparent_idand the server would silently ignore the move. Newfeature-extension file rather than growing
APIClient.swift, per CLAUDE.md.DocumentFolderParentUpdate(.unchanged/.root/.folder) with a hand-writtenencode(to:). The route validates and applies a move whenever theparentIdkey is present,and Swift's synthesized
Encodabledrops aniloptional — so a rename has to omit the keyentirely while a move to root needs an explicit
null. A plainString?can't express both.Views/DocumentsView.swift— Rename in the folder row's context menu and swipe actions,in both the root list and the nested
DocumentFolderView, as an inline text field swapped infor the row. The write is optimistic and rolls the row back to the untouched original if the
server refuses it.
400circular-move rejection and the409duplicate-name conflictboth arrive as
APIError.server(_:)carrying server copy that reads well as-is; the alert showsit verbatim rather than a generic failure string.
Services/AppDataStore.swift—updateDocumentFolder(_:)mirrors the saved folder into thelocal tree and persists it, so the rename survives a cold start. It deliberately enqueues no
sync operation, unlike its
createDocumentFolderOffline/deleteDocumentFolderOfflineneighbours:
POST /api/documents/syncanswers a folder update withunsupported_operation("Folders support only
createanddelete"), so an outbox op would be rejected on every pushand the rename would never persist. Flagged below as a backend follow-up.
Models/Document.swift—DocumentFolder.renamed(to:), for the optimistic value.APIClientDocumentsTests: verb and path,parentIdomitted on rename vs. explicit
nullon move-to-root vs. an id on move, camelCase keys, theabsence of any
folderIdquery (folder routes are path-scoped, not query-scoped), and the400 / 409 / 401 / missing-folder paths.
Testing
xcodebuild -scheme InterlinedList -destination 'platform=iOS Simulator,id=66CAE054-…' build— BUILD SUCCEEDEDxcodebuild … -parallel-testing-enabled NO -skip-testing:InterlinedListTests/E2EReadOnlyTests test— 955 tests, 0 failuresE2E was skipped per the sandbox rules for this run; these changes are covered by the unit suite.
Notes / follow-ups
the rename on it. The API supports it (
parentId: .root/.folder(id), both tested) and the400 cycle rejection is surfaced and rolled back through the same path as rename — there is just
no picker wired into the UI yet, so a move isn't user-reachable.
POST /api/documents/syncrejects folder updates. Until it grows anupdatebranch for
type: "folder", a rename can't be queued offline — it needs the network at themoment it's made, and the local mirror is what carries it across a relaunch.
🤖 Generated with Claude Code