Notes: live folder-sharing (read-only) — Task #445

Owner can share a whole folder with other users; recipients see it
under "Shared with me" in the folders rail and view notes read-only
(cannot edit, add, move, or delete).

- DB: new noteFolderSharesTable (folderId/recipientUserId, cascade,
  unique idx). Pushed via drizzle-kit.
- API (artifacts/api-server/src/routes/notes.ts):
  - GET/PATCH/POST /note-folders return sharedWithCount.
  - GET /note-folders/shared-with-me, GET /note-folders/:id/shares,
    PUT /note-folders/:id/shares (idempotent diff), DELETE
    /note-folders/:id/shares/:userId, GET /note-folders/:id/shared-notes
    (verifies recipient via noteFolderSharesTable, stamps readOnly).
  - Emits note-folder-shared / note-folder-unshared on share changes.
  - Existing note write paths remain owner-scoped → recipients can't
    mutate owner notes.
- Frontend types/hooks (notes-api.ts): SharedFolder, FolderShareRecipient,
  SharedFolderNote/View; useFolderShares, useUpdateFolderShares,
  useSharedWithMeFolders, useSharedFolderNotes.
- FoldersRail: Share menu item, sharedWithCount badge, "Shared with me"
  section, "shared-folder" selection kind, onShareFolder prop.
- notes.tsx: SharedFolderView (no Composer, read-only cards using
  ChecklistView), FolderShareDialog (seeds existing recipients,
  idempotent PUT on save), revocation fallback effect, dialog mount.
- use-notifications-socket.ts: subscribe to note-folder-shared /
  -unshared → invalidate shared-with-me + open shared-folder notes for
  live rail/view refresh.
- i18n: AR/EN keys for share/sharedBy/sharedByName/sharedWithCount/
  sharedWithMe/readOnly/shareRevoked/sharedEmpty/shareSaved/shareFailed.

Pre-existing failing `test` workflow (executive-meetings type errors)
is unrelated and out of scope.
This commit is contained in:
riyadhafraa
2026-05-09 11:07:30 +00:00
parent e287278819
commit a4949983f3
8 changed files with 924 additions and 16 deletions
+28
View File
@@ -106,3 +106,31 @@ export type NoteFolder = typeof noteFoldersTable.$inferSelect;
export type NoteRecipient = typeof noteRecipientsTable.$inferSelect;
export type NoteReply = typeof noteRepliesTable.$inferSelect;
/**
* Folder-level live share. The folder owner (`noteFoldersTable.userId`) grants
* a recipient (`recipientUserId`) read-only access to ALL notes currently in
* the folder. Unlike `noteRecipientsTable`, this is NOT a snapshot — recipients
* always see the owner's current notes via a join, so adds/edits/removes by the
* owner are reflected live in the recipient's "Shared with me" view.
*
* Read-only by design: there is no role column. Recipients cannot edit, add,
* move, or delete notes inside a shared folder; they can only read.
*/
export const noteFolderSharesTable = pgTable("note_folder_shares", {
id: serial("id").primaryKey(),
folderId: integer("folder_id")
.notNull()
.references(() => noteFoldersTable.id, { onDelete: "cascade" }),
recipientUserId: integer("recipient_user_id")
.notNull()
.references(() => usersTable.id, { onDelete: "cascade" }),
sharedAt: timestamp("shared_at", { withTimezone: true }).notNull().defaultNow(),
}, (t) => ({
folderRecipientUnique: uniqueIndex("note_folder_shares_folder_recipient_unique").on(
t.folderId,
t.recipientUserId,
),
}));
export type NoteFolderShare = typeof noteFolderSharesTable.$inferSelect;