Skip to content

feat(documents): rename a document folder in place - #69

Open
Adron wants to merge 1 commit into
mainfrom
feat/rename-document-folder
Open

feat(documents): rename a document folder in place#69
Adron wants to merge 1 commit into
mainfrom
feat/rename-document-folder

Conversation

@Adron

@Adron Adron commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

APIClient could create and delete document folders but not update one, so fixing a typo in a
folder 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 in
the 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:) on
    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. New
    feature-extension file rather than growing APIClient.swift, per CLAUDE.md.
  • DocumentFolderParentUpdate (.unchanged / .root / .folder) with a hand-written
    encode(to:). The route validates and applies a move whenever the parentId key is present,
    and Swift's synthesized Encodable drops a nil optional — so a rename has to omit the key
    entirely while a move to root needs an explicit null. A plain String? 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 in
    for the row. The write is optimistic and rolls the row back to the untouched original if the
    server refuses it.
  • Readable refusals — the 400 circular-move rejection and the 409 duplicate-name conflict
    both arrive as APIError.server(_:) carrying server copy that reads well as-is; the alert shows
    it verbatim rather than a generic failure string.
  • Services/AppDataStore.swiftupdateDocumentFolder(_:) mirrors the saved folder into the
    local tree and persists it, so the rename survives a cold start. It 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. Flagged below as a backend follow-up.
  • Models/Document.swiftDocumentFolder.renamed(to:), for the optimistic value.
  • Tests — 13 request-shape cases in APIClientDocumentsTests: verb and path, parentId
    omitted on rename vs. explicit null on move-to-root vs. an id on move, camelCase keys, the
    absence of any folderId query (folder routes are path-scoped, not query-scoped), and the
    400 / 409 / 401 / missing-folder paths.

Testing

  • xcodebuild -scheme InterlinedList -destination 'platform=iOS Simulator,id=66CAE054-…' buildBUILD SUCCEEDED
  • xcodebuild … -parallel-testing-enabled NO -skip-testing:InterlinedListTests/E2EReadOnlyTests test955 tests, 0 failures

E2E was skipped per the sandbox rules for this run; these changes are covered by the unit suite.

Notes / follow-ups

  • Move-folder UI was left out. The issue lists it as optional and explicitly says not to block
    the rename on it. The API supports it (parentId: .root / .folder(id), both tested) and the
    400 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.
  • Backend ask: POST /api/documents/sync rejects folder updates. Until it grows an update
    branch for type: "folder", a rename can't be queued offline — it needs the network at the
    moment it's made, and the local mirror is what carries it across a relaunch.

🤖 Generated with Claude Code

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
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.

W5: Rename a document folder

1 participant