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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user