notes: user-defined folders with drag-drop (task #413)
DB - New `note_folders` table (per-user unique name); `notes.folder_id` nullable with `ON DELETE SET NULL` so deleting a folder preserves its notes. API - /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped. - /notes POST/PATCH validate folder ownership before assignment (no cross-user folder bleed). - Folder noteCount is tab-aware (active vs archived) via ?archived=. - OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react and api-zod (`pnpm --filter @workspace/api-spec run codegen`). Client - NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder / useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all ["notes", ...] caches; rollback on error). UI (FoldersRail next to My Notes + Archive tabs only) - Desktop: sidebar with All / Unfiled / user folders + counts. - Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid. - Per-folder kebab dropdown with Rename + Delete. - Newly-created folder is auto-selected. - HTML5 draggable note cards on desktop; touch long-press fallback (350ms) for iPad/mobile via shared drop pipeline (registerFolderDropHandler + hit-tested `[data-folder-drop]`). - Dev-only `window.__notesTouchTest` helper (gated to non-production builds) exposes the touch pipeline so e2e tests can drive it deterministically without synthesizing native TouchEvents. Bilingual - notes.folders.* keys added to en.json and ar.json (RTL inherited from existing dir prop wired from i18n.language). Tests (artifacts/tx-os/tests/notes-folders.spec.mjs) - create folder + auto-select - drag note → folder, refresh-and-still-in-folder persistence - filter by folder / Unfiled / All - drag between folders (count math) - rename via kebab - delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL) - cross-user folder rejection (400) - Arabic + RTL render - separate touch test (414×800, hasTouch+isMobile) covers chip-row layout and exercises the touch drop pipeline through window helper. All API node tests + tx-os Playwright suite + tsc clean.
This commit is contained in:
@@ -2905,6 +2905,96 @@ paths:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/NoteReply" }
|
||||
|
||||
# ----- Note folders (user-defined) -----
|
||||
/note-folders:
|
||||
get:
|
||||
operationId: listNoteFolders
|
||||
tags: [notes]
|
||||
summary: List the caller's note folders with per-tab note counts
|
||||
parameters:
|
||||
- in: query
|
||||
name: archived
|
||||
required: false
|
||||
description: Tab scope for noteCount (false = My Notes, true = Archive)
|
||||
schema: { type: boolean, default: false }
|
||||
responses:
|
||||
"200":
|
||||
description: Folders owned by the caller
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/NoteFolder" }
|
||||
post:
|
||||
operationId: createNoteFolder
|
||||
tags: [notes]
|
||||
summary: Create a new note folder owned by the caller
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string, minLength: 1, maxLength: 80 }
|
||||
responses:
|
||||
"201":
|
||||
description: Folder created
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/NoteFolder" }
|
||||
"400":
|
||||
description: Invalid name or duplicate per-user folder name
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
||||
/note-folders/{id}:
|
||||
patch:
|
||||
operationId: updateNoteFolder
|
||||
tags: [notes]
|
||||
summary: Rename a note folder
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string, minLength: 1, maxLength: 80 }
|
||||
responses:
|
||||
"200":
|
||||
description: Folder updated
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/NoteFolder" }
|
||||
"404":
|
||||
description: Folder not found or not owned by caller
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
||||
delete:
|
||||
operationId: deleteNoteFolder
|
||||
tags: [notes]
|
||||
summary: Delete a note folder. Notes inside are preserved (folderId set to null).
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200":
|
||||
description: Folder deleted
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/SuccessResponse" }
|
||||
|
||||
/admin/users/{id}/dependents/notes:
|
||||
get:
|
||||
operationId: getAdminUserDependentNotes
|
||||
@@ -4900,6 +4990,19 @@ components:
|
||||
required:
|
||||
[id, recipientUserId, status, sentAt, readAt, archivedAt]
|
||||
|
||||
NoteFolder:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
userId: { type: integer }
|
||||
name: { type: string }
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
noteCount:
|
||||
type: integer
|
||||
description: Number of notes in this folder, scoped to the requested tab (active vs archived).
|
||||
required: [id, userId, name, createdAt, updatedAt, noteCount]
|
||||
|
||||
SentNote:
|
||||
type: object
|
||||
properties:
|
||||
@@ -4910,6 +5013,9 @@ components:
|
||||
color: { type: string }
|
||||
isPinned: { type: boolean }
|
||||
isArchived: { type: boolean }
|
||||
folderId:
|
||||
type: ["integer", "null"]
|
||||
description: Owning folder id, or null for Unfiled.
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
recipients:
|
||||
@@ -4926,6 +5032,9 @@ components:
|
||||
title: { type: string }
|
||||
content: { type: string }
|
||||
color: { type: string }
|
||||
folderId:
|
||||
type: ["integer", "null"]
|
||||
description: Owning folder id on the recipient's side, or null for Unfiled.
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
sender:
|
||||
|
||||
Reference in New Issue
Block a user