Multi-user sharing, groups, and in-app notifications
Backend: - Alembic migration 0025: groups, group_memberships, project_shares, note_shares, notifications tables - models: Group, GroupMembership, ProjectShare, NoteShare, Notification - services/access.py: permission resolution (viewer/editor/admin/owner) - services/groups.py + routes/groups.py: full group CRUD + membership - services/sharing.py + routes/shares.py: project/note sharing API - services/notifications.py: in-app notification create/list/mark-read - routes/in_app_notifications.py: GET/POST notification endpoints - routes/users.py: user search endpoint - services/projects.py + services/notes.py: *_for_user variants - routes updated to use *_for_user on get/list; adds permission field - app.py: register all new blueprints Frontend: - api/client.ts: ShareEntry, GroupEntry, NotificationEntry types + helpers - stores/notifications.ts: Pinia store with polling - NotificationBell.vue: bell icon + badge + dropdown toggle + 60s poll - NotificationsPanel.vue: unread notification list with mark-all-read - ShareDialog.vue: teleport modal for sharing projects/notes with users/groups - SharedWithMeView.vue: /shared route listing shared projects and notes - AppHeader: NotificationBell, Shared nav link - ProjectView, NoteViewerView, TaskViewerView: Share button + ShareDialog - SettingsView: Groups admin tab with create/delete groups + member mgmt - router: /shared route Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,111 @@ export async function apiDelete(path: string): Promise<void> {
|
||||
return handleResponse<void>(res, path);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sharing, Groups, Notifications, User search
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ShareEntry {
|
||||
id: number
|
||||
permission: string
|
||||
shared_with_user_id: number | null
|
||||
shared_with_group_id: number | null
|
||||
username?: string
|
||||
group_name?: string
|
||||
invited_by: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface GroupEntry {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
created_by: number | null
|
||||
member_count: number
|
||||
is_member: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface GroupMember {
|
||||
id: number
|
||||
group_id: number
|
||||
user_id: number
|
||||
role: string
|
||||
username: string
|
||||
email: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface NotificationEntry {
|
||||
id: number
|
||||
user_id: number
|
||||
type: string
|
||||
payload: Record<string, unknown>
|
||||
read_at: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface UserSearchResult {
|
||||
id: number
|
||||
username: string
|
||||
email: string | null
|
||||
}
|
||||
|
||||
// --- User search ---
|
||||
export const searchUsers = (q: string) =>
|
||||
apiGet<{ users: UserSearchResult[] }>(`/api/users/search?q=${encodeURIComponent(q)}`).then(r => r.users)
|
||||
|
||||
// --- Groups ---
|
||||
export const listGroups = () => apiGet<{ groups: GroupEntry[] }>('/api/groups').then(r => r.groups)
|
||||
export const createGroup = (name: string, description?: string) =>
|
||||
apiPost<GroupEntry>('/api/groups', { name, description })
|
||||
export const updateGroup = (id: number, data: { name?: string; description?: string }) =>
|
||||
apiPatch<GroupEntry>(`/api/groups/${id}`, data)
|
||||
export const deleteGroup = (id: number) => apiDelete(`/api/groups/${id}`)
|
||||
export const getGroupDetail = (id: number) =>
|
||||
apiGet<GroupEntry & { members: GroupMember[] }>(`/api/groups/${id}`)
|
||||
export const listGroupMembers = (id: number) =>
|
||||
apiGet<{ members: GroupMember[] }>(`/api/groups/${id}/members`).then(r => r.members)
|
||||
export const addGroupMember = (groupId: number, userId: number, role: string) =>
|
||||
apiPost<GroupMember>(`/api/groups/${groupId}/members`, { user_id: userId, role })
|
||||
export const updateGroupMember = (groupId: number, userId: number, role: string) =>
|
||||
apiPatch<GroupMember>(`/api/groups/${groupId}/members/${userId}`, { role })
|
||||
export const removeGroupMember = (groupId: number, userId: number) =>
|
||||
apiDelete(`/api/groups/${groupId}/members/${userId}`)
|
||||
|
||||
// --- Project shares ---
|
||||
export const listProjectShares = (projectId: number) =>
|
||||
apiGet<{ shares: ShareEntry[] }>(`/api/projects/${projectId}/shares`).then(r => r.shares)
|
||||
export const createProjectShare = (projectId: number, body: { user_id?: number; group_id?: number; permission: string }) =>
|
||||
apiPost<ShareEntry>(`/api/projects/${projectId}/shares`, body)
|
||||
export const updateProjectShare = (projectId: number, shareId: number, permission: string) =>
|
||||
apiPatch<ShareEntry>(`/api/projects/${projectId}/shares/${shareId}`, { permission })
|
||||
export const deleteProjectShare = (projectId: number, shareId: number) =>
|
||||
apiDelete(`/api/projects/${projectId}/shares/${shareId}`)
|
||||
|
||||
// --- Note / task shares ---
|
||||
export const listNoteShares = (noteId: number) =>
|
||||
apiGet<{ shares: ShareEntry[] }>(`/api/notes/${noteId}/shares`).then(r => r.shares)
|
||||
export const createNoteShare = (noteId: number, body: { user_id?: number; group_id?: number; permission: string }) =>
|
||||
apiPost<ShareEntry>(`/api/notes/${noteId}/shares`, body)
|
||||
export const updateNoteShare = (noteId: number, shareId: number, permission: string) =>
|
||||
apiPatch<ShareEntry>(`/api/notes/${noteId}/shares/${shareId}`, { permission })
|
||||
export const deleteNoteShare = (noteId: number, shareId: number) =>
|
||||
apiDelete(`/api/notes/${noteId}/shares/${shareId}`)
|
||||
|
||||
// --- Shared-with-me ---
|
||||
export const getSharedWithMe = () =>
|
||||
apiGet<{ projects: Record<string, unknown>[]; notes: Record<string, unknown>[] }>('/api/shared-with-me')
|
||||
|
||||
// --- In-app notifications ---
|
||||
export const getNotifications = (all = false) =>
|
||||
apiGet<{ notifications: NotificationEntry[] }>(`/api/notifications${all ? '?all=true' : ''}`).then(r => r.notifications)
|
||||
export const getNotificationCount = () =>
|
||||
apiGet<{ count: number }>('/api/notifications/count').then(r => r.count)
|
||||
export const markNotificationRead = (id: number) => apiPost<void>(`/api/notifications/${id}/read`, {})
|
||||
export const markAllNotificationsRead = () => apiPost<{ marked: number }>('/api/notifications/read-all', {})
|
||||
|
||||
export interface SSEStreamHandle {
|
||||
close(): void;
|
||||
/** Resolves when the stream closes (normally or via error/abort). */
|
||||
|
||||
Reference in New Issue
Block a user