mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-08 14:56:19 +02:00
* feat(squad): accept avatar_url on CreateSquad Threads avatar_url through the SQL query, sqlc-generated code, and the Go handler so the create-squad flow can persist an avatar at creation time instead of forcing a follow-up PATCH. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(squad): add avatar_url to CreateSquadRequest Extends the TS contract for the new backend field so the frontend can pass an uploaded avatar URL through api.createSquad. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(squads): rework Create Squad modal to match CreateAgentDialog (MUL-2233) Replaces the cramped small-dialog flow with the same large-dialog shape used by Create Agent: identity row (AvatarPicker + name + description with char counter), grouped Leader picker (My Agents first, then Workspace Agents), and a new multi-select Additional Members picker covering agents and workspace members. The members trigger collapses to "+N" once more than three are selected; promoting an agent to leader auto-drops it from the additional-members list. After createSquad, additional members are attached via Promise.allSettled so a single failure surfaces a warning toast without blocking navigation — the squad still exists and the user can retry from the Members tab. Adds packages/views/modals/create-squad.test.tsx covering identity binding, leader-group ordering, leader/member conflict sanitization, the empty- and partial-failure success paths, and the create-failure recovery path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(squads): valid trigger HTML + drop conflicted leader from members Two issues from PR #2645 review: 1. AdditionalMembersPicker's PopoverTrigger was a <button> containing MemberChip's remove <button>, which React/HTML flags as nested interactive content (hydration + a11y warning). Render the trigger as a <div role="combobox"> via Base UI's render prop so the chip's remove button is valid. 2. sanitizedMembers only hid the leader from rendered/submitted output, so promoting an additional member to leader then switching leader away resurrected the hidden pick. Drop it from selectedMembers at the moment of promotion via handleLeaderChange; sanitizedMembers is no longer needed. Adds a test that promotes → switches leader and asserts the member is not resubmitted. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
499 lines
13 KiB
Go
499 lines
13 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: squad.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const addSquadMember = `-- name: AddSquadMember :one
|
|
INSERT INTO squad_member (squad_id, member_type, member_id, role)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, squad_id, member_type, member_id, role, created_at
|
|
`
|
|
|
|
type AddSquadMemberParams struct {
|
|
SquadID pgtype.UUID `json:"squad_id"`
|
|
MemberType string `json:"member_type"`
|
|
MemberID pgtype.UUID `json:"member_id"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
func (q *Queries) AddSquadMember(ctx context.Context, arg AddSquadMemberParams) (SquadMember, error) {
|
|
row := q.db.QueryRow(ctx, addSquadMember,
|
|
arg.SquadID,
|
|
arg.MemberType,
|
|
arg.MemberID,
|
|
arg.Role,
|
|
)
|
|
var i SquadMember
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.SquadID,
|
|
&i.MemberType,
|
|
&i.MemberID,
|
|
&i.Role,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const archiveSquad = `-- name: ArchiveSquad :one
|
|
UPDATE squad SET archived_at = now(), archived_by = $2, updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions
|
|
`
|
|
|
|
type ArchiveSquadParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
ArchivedBy pgtype.UUID `json:"archived_by"`
|
|
}
|
|
|
|
func (q *Queries) ArchiveSquad(ctx context.Context, arg ArchiveSquadParams) (Squad, error) {
|
|
row := q.db.QueryRow(ctx, archiveSquad, arg.ID, arg.ArchivedBy)
|
|
var i Squad
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.LeaderID,
|
|
&i.CreatorID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.AvatarUrl,
|
|
&i.Instructions,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const countSquadMembers = `-- name: CountSquadMembers :one
|
|
SELECT count(*) FROM squad_member WHERE squad_id = $1
|
|
`
|
|
|
|
func (q *Queries) CountSquadMembers(ctx context.Context, squadID pgtype.UUID) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countSquadMembers, squadID)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const createSquad = `-- name: CreateSquad :one
|
|
INSERT INTO squad (workspace_id, name, description, leader_id, creator_id, avatar_url)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions
|
|
`
|
|
|
|
type CreateSquadParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
LeaderID pgtype.UUID `json:"leader_id"`
|
|
CreatorID pgtype.UUID `json:"creator_id"`
|
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
|
}
|
|
|
|
func (q *Queries) CreateSquad(ctx context.Context, arg CreateSquadParams) (Squad, error) {
|
|
row := q.db.QueryRow(ctx, createSquad,
|
|
arg.WorkspaceID,
|
|
arg.Name,
|
|
arg.Description,
|
|
arg.LeaderID,
|
|
arg.CreatorID,
|
|
arg.AvatarUrl,
|
|
)
|
|
var i Squad
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.LeaderID,
|
|
&i.CreatorID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.AvatarUrl,
|
|
&i.Instructions,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getSquad = `-- name: GetSquad :one
|
|
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetSquad(ctx context.Context, id pgtype.UUID) (Squad, error) {
|
|
row := q.db.QueryRow(ctx, getSquad, id)
|
|
var i Squad
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.LeaderID,
|
|
&i.CreatorID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.AvatarUrl,
|
|
&i.Instructions,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getSquadByAssignee = `-- name: GetSquadByAssignee :one
|
|
SELECT s.id, s.workspace_id, s.name, s.description, s.leader_id, s.creator_id, s.created_at, s.updated_at, s.archived_at, s.archived_by, s.avatar_url, s.instructions FROM squad s WHERE s.id = $1 AND s.workspace_id = $2
|
|
`
|
|
|
|
type GetSquadByAssigneeParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Look up the squad when an issue is assigned to a squad.
|
|
func (q *Queries) GetSquadByAssignee(ctx context.Context, arg GetSquadByAssigneeParams) (Squad, error) {
|
|
row := q.db.QueryRow(ctx, getSquadByAssignee, arg.ID, arg.WorkspaceID)
|
|
var i Squad
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.LeaderID,
|
|
&i.CreatorID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.AvatarUrl,
|
|
&i.Instructions,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getSquadInWorkspace = `-- name: GetSquadInWorkspace :one
|
|
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetSquadInWorkspaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetSquadInWorkspace(ctx context.Context, arg GetSquadInWorkspaceParams) (Squad, error) {
|
|
row := q.db.QueryRow(ctx, getSquadInWorkspace, arg.ID, arg.WorkspaceID)
|
|
var i Squad
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.LeaderID,
|
|
&i.CreatorID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.AvatarUrl,
|
|
&i.Instructions,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const isSquadMember = `-- name: IsSquadMember :one
|
|
SELECT EXISTS(
|
|
SELECT 1 FROM squad_member
|
|
WHERE squad_id = $1 AND member_type = $2 AND member_id = $3
|
|
) AS is_member
|
|
`
|
|
|
|
type IsSquadMemberParams struct {
|
|
SquadID pgtype.UUID `json:"squad_id"`
|
|
MemberType string `json:"member_type"`
|
|
MemberID pgtype.UUID `json:"member_id"`
|
|
}
|
|
|
|
func (q *Queries) IsSquadMember(ctx context.Context, arg IsSquadMemberParams) (bool, error) {
|
|
row := q.db.QueryRow(ctx, isSquadMember, arg.SquadID, arg.MemberType, arg.MemberID)
|
|
var is_member bool
|
|
err := row.Scan(&is_member)
|
|
return is_member, err
|
|
}
|
|
|
|
const listAllSquads = `-- name: ListAllSquads :many
|
|
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad WHERE workspace_id = $1 ORDER BY created_at ASC
|
|
`
|
|
|
|
func (q *Queries) ListAllSquads(ctx context.Context, workspaceID pgtype.UUID) ([]Squad, error) {
|
|
rows, err := q.db.Query(ctx, listAllSquads, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Squad{}
|
|
for rows.Next() {
|
|
var i Squad
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.LeaderID,
|
|
&i.CreatorID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.AvatarUrl,
|
|
&i.Instructions,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSquadMembers = `-- name: ListSquadMembers :many
|
|
SELECT id, squad_id, member_type, member_id, role, created_at FROM squad_member WHERE squad_id = $1 ORDER BY created_at ASC
|
|
`
|
|
|
|
func (q *Queries) ListSquadMembers(ctx context.Context, squadID pgtype.UUID) ([]SquadMember, error) {
|
|
rows, err := q.db.Query(ctx, listSquadMembers, squadID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []SquadMember{}
|
|
for rows.Next() {
|
|
var i SquadMember
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.SquadID,
|
|
&i.MemberType,
|
|
&i.MemberID,
|
|
&i.Role,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSquads = `-- name: ListSquads :many
|
|
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad WHERE workspace_id = $1 AND archived_at IS NULL ORDER BY created_at ASC
|
|
`
|
|
|
|
func (q *Queries) ListSquads(ctx context.Context, workspaceID pgtype.UUID) ([]Squad, error) {
|
|
rows, err := q.db.Query(ctx, listSquads, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Squad{}
|
|
for rows.Next() {
|
|
var i Squad
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.LeaderID,
|
|
&i.CreatorID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.AvatarUrl,
|
|
&i.Instructions,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSquadsByMember = `-- name: ListSquadsByMember :many
|
|
SELECT s.id, s.workspace_id, s.name, s.description, s.leader_id, s.creator_id, s.created_at, s.updated_at, s.archived_at, s.archived_by, s.avatar_url, s.instructions FROM squad s
|
|
JOIN squad_member sm ON sm.squad_id = s.id
|
|
WHERE s.workspace_id = $1 AND sm.member_type = $2 AND sm.member_id = $3
|
|
ORDER BY s.created_at ASC
|
|
`
|
|
|
|
type ListSquadsByMemberParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
MemberType string `json:"member_type"`
|
|
MemberID pgtype.UUID `json:"member_id"`
|
|
}
|
|
|
|
// Find all squads a given entity belongs to in a workspace.
|
|
func (q *Queries) ListSquadsByMember(ctx context.Context, arg ListSquadsByMemberParams) ([]Squad, error) {
|
|
rows, err := q.db.Query(ctx, listSquadsByMember, arg.WorkspaceID, arg.MemberType, arg.MemberID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Squad{}
|
|
for rows.Next() {
|
|
var i Squad
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.LeaderID,
|
|
&i.CreatorID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.AvatarUrl,
|
|
&i.Instructions,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const removeSquadMember = `-- name: RemoveSquadMember :execrows
|
|
DELETE FROM squad_member
|
|
WHERE squad_id = $1 AND member_type = $2 AND member_id = $3
|
|
`
|
|
|
|
type RemoveSquadMemberParams struct {
|
|
SquadID pgtype.UUID `json:"squad_id"`
|
|
MemberType string `json:"member_type"`
|
|
MemberID pgtype.UUID `json:"member_id"`
|
|
}
|
|
|
|
func (q *Queries) RemoveSquadMember(ctx context.Context, arg RemoveSquadMemberParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, removeSquadMember, arg.SquadID, arg.MemberType, arg.MemberID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const transferSquadAssignees = `-- name: TransferSquadAssignees :exec
|
|
UPDATE issue SET assignee_type = 'agent', assignee_id = $2, updated_at = now()
|
|
WHERE assignee_type = 'squad' AND assignee_id = $1
|
|
`
|
|
|
|
type TransferSquadAssigneesParams struct {
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
AssigneeID_2 pgtype.UUID `json:"assignee_id_2"`
|
|
}
|
|
|
|
// Transfer all issues assigned to a squad to the squad's leader agent.
|
|
func (q *Queries) TransferSquadAssignees(ctx context.Context, arg TransferSquadAssigneesParams) error {
|
|
_, err := q.db.Exec(ctx, transferSquadAssignees, arg.AssigneeID, arg.AssigneeID_2)
|
|
return err
|
|
}
|
|
|
|
const updateSquad = `-- name: UpdateSquad :one
|
|
UPDATE squad SET
|
|
name = COALESCE($2, name),
|
|
description = COALESCE($3, description),
|
|
leader_id = COALESCE($4, leader_id),
|
|
avatar_url = COALESCE($5, avatar_url),
|
|
instructions = COALESCE($6, instructions),
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions
|
|
`
|
|
|
|
type UpdateSquadParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Name pgtype.Text `json:"name"`
|
|
Description pgtype.Text `json:"description"`
|
|
LeaderID pgtype.UUID `json:"leader_id"`
|
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
|
Instructions pgtype.Text `json:"instructions"`
|
|
}
|
|
|
|
func (q *Queries) UpdateSquad(ctx context.Context, arg UpdateSquadParams) (Squad, error) {
|
|
row := q.db.QueryRow(ctx, updateSquad,
|
|
arg.ID,
|
|
arg.Name,
|
|
arg.Description,
|
|
arg.LeaderID,
|
|
arg.AvatarUrl,
|
|
arg.Instructions,
|
|
)
|
|
var i Squad
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.LeaderID,
|
|
&i.CreatorID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.AvatarUrl,
|
|
&i.Instructions,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateSquadMemberRole = `-- name: UpdateSquadMemberRole :one
|
|
UPDATE squad_member SET role = $4
|
|
WHERE squad_id = $1 AND member_type = $2 AND member_id = $3
|
|
RETURNING id, squad_id, member_type, member_id, role, created_at
|
|
`
|
|
|
|
type UpdateSquadMemberRoleParams struct {
|
|
SquadID pgtype.UUID `json:"squad_id"`
|
|
MemberType string `json:"member_type"`
|
|
MemberID pgtype.UUID `json:"member_id"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
func (q *Queries) UpdateSquadMemberRole(ctx context.Context, arg UpdateSquadMemberRoleParams) (SquadMember, error) {
|
|
row := q.db.QueryRow(ctx, updateSquadMemberRole,
|
|
arg.SquadID,
|
|
arg.MemberType,
|
|
arg.MemberID,
|
|
arg.Role,
|
|
)
|
|
var i SquadMember
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.SquadID,
|
|
&i.MemberType,
|
|
&i.MemberID,
|
|
&i.Role,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|