diff --git a/apps/web/app/(dashboard)/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/_components/app-sidebar.tsx index 5c531ef1..c3caca71 100644 --- a/apps/web/app/(dashboard)/_components/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/_components/app-sidebar.tsx @@ -20,7 +20,6 @@ import { WorkspaceAvatar } from "@/features/workspace"; import { Sidebar, SidebarContent, - SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarHeader, @@ -43,11 +42,14 @@ import { useWorkspaceStore } from "@/features/workspace"; import { useInboxStore } from "@/features/inbox"; import { useModalStore } from "@/features/modals"; -const navItems = [ +const primaryNav = [ { href: "/inbox", label: "Inbox", icon: Inbox }, + { href: "/issues", label: "Issues", icon: ListTodo }, +]; + +const workspaceNav = [ { href: "/agents", label: "Agents", icon: Bot }, { href: "/skills", label: "Skills", icon: Sparkles }, - { href: "/issues", label: "Issues", icon: ListTodo }, { href: "/knowledge-base", label: "Knowledge Base", icon: BookOpen }, ]; @@ -180,7 +182,7 @@ export function AppSidebar() { - {navItems.map((item) => { + {primaryNav.map((item) => { const isActive = pathname === item.href; return ( @@ -203,28 +205,29 @@ export function AppSidebar() { - - {/* User */} - - {user && ( - - - -
- {user.name - .split(" ") - .map((w) => w[0]) - .join("") - .toUpperCase() - .slice(0, 2)} -
- {user.name} -
-
-
- )} -
+ + + + {workspaceNav.map((item) => { + const isActive = pathname === item.href; + return ( + + } + className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground" + > + + {item.label} + + + ); + })} + + + + ); } diff --git a/apps/web/app/(dashboard)/inbox/page.tsx b/apps/web/app/(dashboard)/inbox/page.tsx index 1b00a14e..32440798 100644 --- a/apps/web/app/(dashboard)/inbox/page.tsx +++ b/apps/web/app/(dashboard)/inbox/page.tsx @@ -2,8 +2,7 @@ import { useState, useMemo } from "react"; import { useInboxStore } from "@/features/inbox"; -import { useActorName } from "@/features/workspace"; -import { IssueDetail } from "@/features/issues/components"; +import { IssueDetail, StatusIcon } from "@/features/issues/components"; import { ActorAvatar } from "@/components/common/actor-avatar"; import { toast } from "sonner"; import { @@ -64,14 +63,10 @@ function InboxListItem({ item, isSelected, onClick, - getActorName, - getActorInitials, }: { item: InboxItem; isSelected: boolean; onClick: () => void; - getActorName: (type: string, id: string) => string; - getActorInitials: (type: string, id: string) => string; }) { return ( ); } @@ -118,7 +118,6 @@ export default function InboxPage() { const storeItems = useInboxStore((s) => s.items); const loading = useInboxStore((s) => s.loading); - const { getActorName, getActorInitials } = useActorName(); // Sort: severity first, then newest first const items = useMemo(() => { @@ -253,7 +252,7 @@ export default function InboxPage() { > - + Mark all as read @@ -288,8 +287,6 @@ export default function InboxPage() { item={item} isSelected={item.id === selectedId} onClick={() => handleSelect(item)} - getActorName={getActorName} - getActorInitials={getActorInitials} /> ))} @@ -297,7 +294,7 @@ export default function InboxPage() { {/* Right column — detail */} -
+
{selected?.issue_id ? ( + {children} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 0df6bb65..395fd882 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -29,7 +29,7 @@ export default function RootLayout({ diff --git a/apps/web/components/common/actor-avatar.tsx b/apps/web/components/common/actor-avatar.tsx index c9133a7a..b6bc740a 100644 --- a/apps/web/components/common/actor-avatar.tsx +++ b/apps/web/components/common/actor-avatar.tsx @@ -1,5 +1,8 @@ +"use client"; + import { Bot } from "lucide-react"; import { cn } from "@/lib/utils"; +import { useActorName } from "@/features/workspace"; interface ActorAvatarProps { actorType: string; @@ -18,8 +21,12 @@ function ActorAvatar({ getInitials, className, }: ActorAvatarProps) { - const name = getName?.(actorType, actorId); - const initials = getInitials?.(actorType, actorId); + const actorNameHook = useActorName(); + const resolveName = getName ?? actorNameHook.getActorName; + const resolveInitials = getInitials ?? actorNameHook.getActorInitials; + + const name = resolveName(actorType, actorId); + const initials = resolveInitials(actorType, actorId); const isAgent = actorType === "agent"; return ( diff --git a/apps/web/features/inbox/store.ts b/apps/web/features/inbox/store.ts index 2a97a0d3..b0283620 100644 --- a/apps/web/features/inbox/store.ts +++ b/apps/web/features/inbox/store.ts @@ -1,7 +1,7 @@ "use client"; import { create } from "zustand"; -import type { InboxItem } from "@multica/types"; +import type { InboxItem, IssueStatus } from "@multica/types"; import { api } from "@/shared/api"; import { createLogger } from "@/shared/logger"; @@ -18,6 +18,7 @@ interface InboxState { markAllRead: () => void; archiveAll: () => void; archiveAllRead: () => void; + updateIssueStatus: (issueId: string, status: IssueStatus) => void; unreadCount: () => number; } @@ -67,5 +68,11 @@ export const useInboxStore = create((set, get) => ({ i.read && !i.archived ? { ...i, archived: true } : i ), })), + updateIssueStatus: (issueId, status) => + set((s) => ({ + items: s.items.map((i) => + i.issue_id === issueId ? { ...i, issue_status: status } : i + ), + })), unreadCount: () => get().items.filter((i) => !i.read && !i.archived).length, })); diff --git a/apps/web/features/issues/components/issue-detail.tsx b/apps/web/features/issues/components/issue-detail.tsx index f556e141..920b22dd 100644 --- a/apps/web/features/issues/components/issue-detail.tsx +++ b/apps/web/features/issues/components/issue-detail.tsx @@ -322,7 +322,7 @@ export function IssueDetail({ issueId, showBreadcrumb, onDelete }: IssueDetailPr const id = issueId; const router = useRouter(); const user = useAuthStore((s) => s.user); - const { getActorName, getActorInitials } = useActorName(); + const { getActorName } = useActorName(); const [issue, setIssue] = useState(null); const [comments, setComments] = useState([]); const [loading, setLoading] = useState(true); @@ -621,8 +621,6 @@ export function IssueDetail({ issueId, showBreadcrumb, onDelete }: IssueDetailPr actorType={comment.author_type} actorId={comment.author_id} size={28} - getName={getActorName} - getInitials={getActorInitials} /> {getActorName(comment.author_type, comment.author_id)} @@ -737,8 +735,6 @@ export function IssueDetail({ issueId, showBreadcrumb, onDelete }: IssueDetailPr actorType={issue.creator_type} actorId={issue.creator_id} size={18} - getName={getActorName} - getInitials={getActorInitials} /> {getActorName(issue.creator_type, issue.creator_id)} diff --git a/apps/web/features/realtime/use-realtime-sync.ts b/apps/web/features/realtime/use-realtime-sync.ts index a372813d..3a850d9c 100644 --- a/apps/web/features/realtime/use-realtime-sync.ts +++ b/apps/web/features/realtime/use-realtime-sync.ts @@ -44,6 +44,7 @@ export function useRealtimeSync(ws: WSClient | null) { ws.on("issue:updated", (p) => { const { issue } = p as IssueUpdatedPayload; useIssueStore.getState().updateIssue(issue.id, issue); + useInboxStore.getState().updateIssueStatus(issue.id, issue.status); }), ws.on("issue:deleted", (p) => { const { issue_id } = p as IssueDeletedPayload; diff --git a/packages/types/src/inbox.ts b/packages/types/src/inbox.ts index 4ff91fe7..19339f5e 100644 --- a/packages/types/src/inbox.ts +++ b/packages/types/src/inbox.ts @@ -1,3 +1,5 @@ +import type { IssueStatus } from "./issue"; + export type InboxSeverity = "action_required" | "attention" | "info"; export type InboxItemType = @@ -13,11 +15,14 @@ export interface InboxItem { workspace_id: string; recipient_type: "member" | "agent"; recipient_id: string; + actor_type: "member" | "agent" | null; + actor_id: string | null; type: InboxItemType; severity: InboxSeverity; issue_id: string | null; title: string; body: string | null; + issue_status: IssueStatus | null; read: boolean; archived: boolean; created_at: string; diff --git a/server/cmd/server/inbox_listeners.go b/server/cmd/server/inbox_listeners.go index 56cab640..1cdeb232 100644 --- a/server/cmd/server/inbox_listeners.go +++ b/server/cmd/server/inbox_listeners.go @@ -38,18 +38,23 @@ func registerInboxListeners(bus *events.Bus, queries *db.Queries) { IssueID: parseUUID(issue.ID), Title: "New issue assigned: " + issue.Title, Body: util.PtrToText(issue.Description), + ActorType: util.StrToText(e.ActorType), + ActorID: parseUUID(e.ActorID), }) if err != nil { slog.Error("inbox item creation failed", "event", "issue:created", "error", err) return } + resp := inboxItemToResponse(item) + resp["issue_status"] = issue.Status + bus.Publish(events.Event{ Type: protocol.EventInboxNew, WorkspaceID: e.WorkspaceID, ActorType: e.ActorType, ActorID: e.ActorID, - Payload: map[string]any{"item": inboxItemToResponse(item)}, + Payload: map[string]any{"item": resp}, }) }) @@ -84,14 +89,18 @@ func registerInboxListeners(bus *events.Bus, queries *db.Queries) { Severity: "info", IssueID: parseUUID(issue.ID), Title: "Unassigned from: " + issue.Title, + ActorType: util.StrToText(e.ActorType), + ActorID: parseUUID(e.ActorID), }) if err == nil { + oldResp := inboxItemToResponse(oldItem) + oldResp["issue_status"] = issue.Status bus.Publish(events.Event{ Type: protocol.EventInboxNew, WorkspaceID: e.WorkspaceID, ActorType: e.ActorType, ActorID: actorID, - Payload: map[string]any{"item": inboxItemToResponse(oldItem)}, + Payload: map[string]any{"item": oldResp}, }) } } @@ -106,14 +115,18 @@ func registerInboxListeners(bus *events.Bus, queries *db.Queries) { Severity: "action_required", IssueID: parseUUID(issue.ID), Title: "Assigned to you: " + issue.Title, + ActorType: util.StrToText(e.ActorType), + ActorID: parseUUID(e.ActorID), }) if err == nil { + newResp := inboxItemToResponse(newItem) + newResp["issue_status"] = issue.Status bus.Publish(events.Event{ Type: protocol.EventInboxNew, WorkspaceID: e.WorkspaceID, ActorType: e.ActorType, ActorID: actorID, - Payload: map[string]any{"item": inboxItemToResponse(newItem)}, + Payload: map[string]any{"item": newResp}, }) } } @@ -130,14 +143,18 @@ func registerInboxListeners(bus *events.Bus, queries *db.Queries) { Severity: "info", IssueID: parseUUID(issue.ID), Title: issue.Title + " moved to " + issue.Status, + ActorType: util.StrToText(e.ActorType), + ActorID: parseUUID(e.ActorID), }) if err == nil { + aResp := inboxItemToResponse(aItem) + aResp["issue_status"] = issue.Status bus.Publish(events.Event{ Type: protocol.EventInboxNew, WorkspaceID: e.WorkspaceID, ActorType: e.ActorType, ActorID: actorID, - Payload: map[string]any{"item": inboxItemToResponse(aItem)}, + Payload: map[string]any{"item": aResp}, }) } } @@ -155,14 +172,18 @@ func registerInboxListeners(bus *events.Bus, queries *db.Queries) { Severity: "info", IssueID: parseUUID(issue.ID), Title: "Status changed: " + issue.Title, + ActorType: util.StrToText(e.ActorType), + ActorID: parseUUID(e.ActorID), }) if err == nil { + cResp := inboxItemToResponse(cItem) + cResp["issue_status"] = issue.Status bus.Publish(events.Event{ Type: protocol.EventInboxNew, WorkspaceID: e.WorkspaceID, ActorType: e.ActorType, ActorID: actorID, - Payload: map[string]any{"item": inboxItemToResponse(cItem)}, + Payload: map[string]any{"item": cResp}, }) } } @@ -183,6 +204,7 @@ func registerInboxListeners(bus *events.Bus, queries *db.Queries) { issueTitle, _ := payload["issue_title"].(string) issueAssigneeType, _ := payload["issue_assignee_type"].(*string) issueAssigneeID, _ := payload["issue_assignee_id"].(*string) + issueStatus, _ := payload["issue_status"].(string) // Only notify if assignee is a member and is not the commenter if issueAssigneeType == nil || issueAssigneeID == nil { @@ -201,18 +223,23 @@ func registerInboxListeners(bus *events.Bus, queries *db.Queries) { IssueID: parseUUID(comment.IssueID), Title: "New comment on: " + issueTitle, Body: util.StrToText(comment.Content), + ActorType: util.StrToText(e.ActorType), + ActorID: parseUUID(e.ActorID), }) if err != nil { slog.Error("inbox item creation failed", "event", "comment:created", "error", err) return } + commentResp := inboxItemToResponse(item) + commentResp["issue_status"] = issueStatus + bus.Publish(events.Event{ Type: protocol.EventInboxNew, WorkspaceID: e.WorkspaceID, ActorType: e.ActorType, ActorID: e.ActorID, - Payload: map[string]any{"item": inboxItemToResponse(item)}, + Payload: map[string]any{"item": commentResp}, }) }) } @@ -233,5 +260,7 @@ func inboxItemToResponse(item db.InboxItem) map[string]any { "read": item.Read, "archived": item.Archived, "created_at": util.TimestampToString(item.CreatedAt), + "actor_type": util.TextToPtr(item.ActorType), + "actor_id": util.UUIDToPtr(item.ActorID), } } diff --git a/server/internal/handler/comment.go b/server/internal/handler/comment.go index 3a841d24..ea40c200 100644 --- a/server/internal/handler/comment.go +++ b/server/internal/handler/comment.go @@ -103,10 +103,11 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) { resp := commentToResponse(comment) slog.Info("comment created", append(logger.RequestAttrs(r), "comment_id", uuidToString(comment.ID), "issue_id", issueID)...) h.publish(protocol.EventCommentCreated, uuidToString(issue.WorkspaceID), "member", userID, map[string]any{ - "comment": resp, - "issue_title": issue.Title, - "issue_assignee_type": textToPtr(issue.AssigneeType), - "issue_assignee_id": uuidToPtr(issue.AssigneeID), + "comment": resp, + "issue_title": issue.Title, + "issue_assignee_type": textToPtr(issue.AssigneeType), + "issue_assignee_id": uuidToPtr(issue.AssigneeID), + "issue_status": issue.Status, }) writeJSON(w, http.StatusCreated, resp) diff --git a/server/internal/handler/inbox.go b/server/internal/handler/inbox.go index 989cb4e9..38c513a4 100644 --- a/server/internal/handler/inbox.go +++ b/server/internal/handler/inbox.go @@ -24,6 +24,9 @@ type InboxItemResponse struct { Read bool `json:"read"` Archived bool `json:"archived"` CreatedAt string `json:"created_at"` + IssueStatus *string `json:"issue_status"` + ActorType *string `json:"actor_type"` + ActorID *string `json:"actor_id"` } func inboxToResponse(i db.InboxItem) InboxItemResponse { @@ -40,6 +43,28 @@ func inboxToResponse(i db.InboxItem) InboxItemResponse { Read: i.Read, Archived: i.Archived, CreatedAt: timestampToString(i.CreatedAt), + ActorType: textToPtr(i.ActorType), + ActorID: uuidToPtr(i.ActorID), + } +} + +func inboxRowToResponse(r db.ListInboxItemsRow) InboxItemResponse { + return InboxItemResponse{ + ID: uuidToString(r.ID), + WorkspaceID: uuidToString(r.WorkspaceID), + RecipientType: r.RecipientType, + RecipientID: uuidToString(r.RecipientID), + Type: r.Type, + Severity: r.Severity, + IssueID: uuidToPtr(r.IssueID), + Title: r.Title, + Body: textToPtr(r.Body), + Read: r.Read, + Archived: r.Archived, + CreatedAt: timestampToString(r.CreatedAt), + IssueStatus: textToPtr(r.IssueStatus), + ActorType: textToPtr(r.ActorType), + ActorID: uuidToPtr(r.ActorID), } } @@ -75,7 +100,7 @@ func (h *Handler) ListInbox(w http.ResponseWriter, r *http.Request) { resp := make([]InboxItemResponse, len(items)) for i, item := range items { - resp[i] = inboxToResponse(item) + resp[i] = inboxRowToResponse(item) } writeJSON(w, http.StatusOK, resp) diff --git a/server/migrations/009_inbox_actor.down.sql b/server/migrations/009_inbox_actor.down.sql new file mode 100644 index 00000000..fd7dc26b --- /dev/null +++ b/server/migrations/009_inbox_actor.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE inbox_item DROP COLUMN IF EXISTS actor_type; +ALTER TABLE inbox_item DROP COLUMN IF EXISTS actor_id; diff --git a/server/migrations/009_inbox_actor.up.sql b/server/migrations/009_inbox_actor.up.sql new file mode 100644 index 00000000..81afe758 --- /dev/null +++ b/server/migrations/009_inbox_actor.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE inbox_item ADD COLUMN actor_type TEXT; +ALTER TABLE inbox_item ADD COLUMN actor_id UUID; diff --git a/server/pkg/db/generated/inbox.sql.go b/server/pkg/db/generated/inbox.sql.go index 2ad4dca1..7c8ba2d9 100644 --- a/server/pkg/db/generated/inbox.sql.go +++ b/server/pkg/db/generated/inbox.sql.go @@ -54,7 +54,7 @@ func (q *Queries) ArchiveCompletedInbox(ctx context.Context, recipientID pgtype. const archiveInboxItem = `-- name: ArchiveInboxItem :one UPDATE inbox_item SET archived = true WHERE id = $1 -RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at +RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id ` func (q *Queries) ArchiveInboxItem(ctx context.Context, id pgtype.UUID) (InboxItem, error) { @@ -73,6 +73,8 @@ func (q *Queries) ArchiveInboxItem(ctx context.Context, id pgtype.UUID) (InboxIt &i.Read, &i.Archived, &i.CreatedAt, + &i.ActorType, + &i.ActorID, ) return i, err } @@ -97,9 +99,10 @@ func (q *Queries) CountUnreadInbox(ctx context.Context, arg CountUnreadInboxPara const createInboxItem = `-- name: CreateInboxItem :one INSERT INTO inbox_item ( workspace_id, recipient_type, recipient_id, - type, severity, issue_id, title, body -) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) -RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at + type, severity, issue_id, title, body, + actor_type, actor_id +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id ` type CreateInboxItemParams struct { @@ -111,6 +114,8 @@ type CreateInboxItemParams struct { IssueID pgtype.UUID `json:"issue_id"` Title string `json:"title"` Body pgtype.Text `json:"body"` + ActorType pgtype.Text `json:"actor_type"` + ActorID pgtype.UUID `json:"actor_id"` } func (q *Queries) CreateInboxItem(ctx context.Context, arg CreateInboxItemParams) (InboxItem, error) { @@ -123,6 +128,8 @@ func (q *Queries) CreateInboxItem(ctx context.Context, arg CreateInboxItemParams arg.IssueID, arg.Title, arg.Body, + arg.ActorType, + arg.ActorID, ) var i InboxItem err := row.Scan( @@ -138,12 +145,14 @@ func (q *Queries) CreateInboxItem(ctx context.Context, arg CreateInboxItemParams &i.Read, &i.Archived, &i.CreatedAt, + &i.ActorType, + &i.ActorID, ) return i, err } const getInboxItem = `-- name: GetInboxItem :one -SELECT id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at FROM inbox_item +SELECT id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id FROM inbox_item WHERE id = $1 ` @@ -163,14 +172,19 @@ func (q *Queries) GetInboxItem(ctx context.Context, id pgtype.UUID) (InboxItem, &i.Read, &i.Archived, &i.CreatedAt, + &i.ActorType, + &i.ActorID, ) return i, err } const listInboxItems = `-- name: ListInboxItems :many -SELECT id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at FROM inbox_item -WHERE recipient_type = $1 AND recipient_id = $2 AND archived = false -ORDER BY created_at DESC +SELECT i.id, i.workspace_id, i.recipient_type, i.recipient_id, i.type, i.severity, i.issue_id, i.title, i.body, i.read, i.archived, i.created_at, i.actor_type, i.actor_id, + iss.status as issue_status +FROM inbox_item i +LEFT JOIN issue iss ON iss.id = i.issue_id +WHERE i.recipient_type = $1 AND i.recipient_id = $2 AND i.archived = false +ORDER BY i.created_at DESC LIMIT $3 OFFSET $4 ` @@ -181,7 +195,25 @@ type ListInboxItemsParams struct { Offset int32 `json:"offset"` } -func (q *Queries) ListInboxItems(ctx context.Context, arg ListInboxItemsParams) ([]InboxItem, error) { +type ListInboxItemsRow struct { + ID pgtype.UUID `json:"id"` + WorkspaceID pgtype.UUID `json:"workspace_id"` + RecipientType string `json:"recipient_type"` + RecipientID pgtype.UUID `json:"recipient_id"` + Type string `json:"type"` + Severity string `json:"severity"` + IssueID pgtype.UUID `json:"issue_id"` + Title string `json:"title"` + Body pgtype.Text `json:"body"` + Read bool `json:"read"` + Archived bool `json:"archived"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + ActorType pgtype.Text `json:"actor_type"` + ActorID pgtype.UUID `json:"actor_id"` + IssueStatus pgtype.Text `json:"issue_status"` +} + +func (q *Queries) ListInboxItems(ctx context.Context, arg ListInboxItemsParams) ([]ListInboxItemsRow, error) { rows, err := q.db.Query(ctx, listInboxItems, arg.RecipientType, arg.RecipientID, @@ -192,9 +224,9 @@ func (q *Queries) ListInboxItems(ctx context.Context, arg ListInboxItemsParams) return nil, err } defer rows.Close() - items := []InboxItem{} + items := []ListInboxItemsRow{} for rows.Next() { - var i InboxItem + var i ListInboxItemsRow if err := rows.Scan( &i.ID, &i.WorkspaceID, @@ -208,6 +240,9 @@ func (q *Queries) ListInboxItems(ctx context.Context, arg ListInboxItemsParams) &i.Read, &i.Archived, &i.CreatedAt, + &i.ActorType, + &i.ActorID, + &i.IssueStatus, ); err != nil { return nil, err } @@ -235,7 +270,7 @@ func (q *Queries) MarkAllInboxRead(ctx context.Context, recipientID pgtype.UUID) const markInboxRead = `-- name: MarkInboxRead :one UPDATE inbox_item SET read = true WHERE id = $1 -RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at +RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id ` func (q *Queries) MarkInboxRead(ctx context.Context, id pgtype.UUID) (InboxItem, error) { @@ -254,6 +289,8 @@ func (q *Queries) MarkInboxRead(ctx context.Context, id pgtype.UUID) (InboxItem, &i.Read, &i.Archived, &i.CreatedAt, + &i.ActorType, + &i.ActorID, ) return i, err } diff --git a/server/pkg/db/generated/models.go b/server/pkg/db/generated/models.go index a32c05e0..2a16ccff 100644 --- a/server/pkg/db/generated/models.go +++ b/server/pkg/db/generated/models.go @@ -128,6 +128,8 @@ type InboxItem struct { Read bool `json:"read"` Archived bool `json:"archived"` CreatedAt pgtype.Timestamptz `json:"created_at"` + ActorType pgtype.Text `json:"actor_type"` + ActorID pgtype.UUID `json:"actor_id"` } type Issue struct { diff --git a/server/pkg/db/queries/inbox.sql b/server/pkg/db/queries/inbox.sql index d609897a..eed2e2b8 100644 --- a/server/pkg/db/queries/inbox.sql +++ b/server/pkg/db/queries/inbox.sql @@ -1,7 +1,10 @@ -- name: ListInboxItems :many -SELECT * FROM inbox_item -WHERE recipient_type = $1 AND recipient_id = $2 AND archived = false -ORDER BY created_at DESC +SELECT i.*, + iss.status as issue_status +FROM inbox_item i +LEFT JOIN issue iss ON iss.id = i.issue_id +WHERE i.recipient_type = $1 AND i.recipient_id = $2 AND i.archived = false +ORDER BY i.created_at DESC LIMIT $3 OFFSET $4; -- name: GetInboxItem :one @@ -11,8 +14,9 @@ WHERE id = $1; -- name: CreateInboxItem :one INSERT INTO inbox_item ( workspace_id, recipient_type, recipient_id, - type, severity, issue_id, title, body -) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + type, severity, issue_id, title, body, + actor_type, actor_id +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *; -- name: MarkInboxRead :one