mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-11 02:46:45 +08:00
* feat(issues): server-side label + filter querying for issue list Extends GET /api/issues with label_ids, priorities, creator_ids, project_ids, include_no_assignee, and include_no_project params, and moves the existing single-value filters onto array-form. Each filter becomes part of the SQL WHERE clause so paginated buckets reflect the user's selection — fixes the bug where client-side filtering hid matches sitting past the first page (#1491). CLI gains a repeatable --label flag; legacy --priority/--assignee/ --project keep working via the single-value compatibility paths. * feat(issues): drive workspace + my-issues filters from the server issueListOptions and myIssueListOptions now key the React Query cache on a normalized filter object, so each filter combination has its own cache entry and a filter change re-fetches with the wire-shape filter applied server-side. Drops the client-side filterIssues step on the issues page, my-issues page, and project detail — that step silently hid matches that lived past the first paginated page (#1491). Adds a Label submenu to the workspace issues filter dropdown, plus labelFilters in the view store. Mutations and ws-updaters fan their optimistic patches across every filter-keyed list cache via qc.setQueriesData on issueKeys.listPrefix(wsId), and the editor's mention-suggestion reads from any matching list cache for instant first paint regardless of which filter is active. * fix(issues): route Members/Agents scope through server-side filter The Members/Agents scope tabs on the workspace issues page were still narrowing client-side via `assignee_type === 'member'`. That hits the exact pagination-blind bug this PR is meant to fix: if the first 50 issues per status don't include the right assignee type, the tab shows "No issues" while later pages have matches. Adds an `assignee_types text[]` filter to ListIssues / ListOpenIssues / CountIssues, threads it through the API client, normalizer and view filter, and maps the scope tab to it. Each scope now keys its own list cache and refetches with the correct first page. Also disables the My Issues "My Agents" query when the user owns no agents — `assignee_ids: []` was getting dropped by both the API client and the query-key normalizer, so the request went out unfiltered and surfaced unrelated issues under "My Agents".
846 lines
24 KiB
Go
846 lines
24 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: issue.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const childIssueProgress = `-- name: ChildIssueProgress :many
|
|
SELECT parent_issue_id,
|
|
COUNT(*)::bigint AS total,
|
|
COUNT(*) FILTER (WHERE status IN ('done', 'cancelled'))::bigint AS done
|
|
FROM issue
|
|
WHERE workspace_id = $1
|
|
AND parent_issue_id IS NOT NULL
|
|
GROUP BY parent_issue_id
|
|
`
|
|
|
|
type ChildIssueProgressRow struct {
|
|
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
|
|
Total int64 `json:"total"`
|
|
Done int64 `json:"done"`
|
|
}
|
|
|
|
func (q *Queries) ChildIssueProgress(ctx context.Context, workspaceID pgtype.UUID) ([]ChildIssueProgressRow, error) {
|
|
rows, err := q.db.Query(ctx, childIssueProgress, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ChildIssueProgressRow{}
|
|
for rows.Next() {
|
|
var i ChildIssueProgressRow
|
|
if err := rows.Scan(&i.ParentIssueID, &i.Total, &i.Done); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const countCreatedIssueAssignees = `-- name: CountCreatedIssueAssignees :many
|
|
SELECT
|
|
assignee_type,
|
|
assignee_id,
|
|
COUNT(*)::bigint as frequency
|
|
FROM issue
|
|
WHERE workspace_id = $1
|
|
AND creator_id = $2
|
|
AND creator_type = 'member'
|
|
AND assignee_type IS NOT NULL
|
|
AND assignee_id IS NOT NULL
|
|
GROUP BY assignee_type, assignee_id
|
|
`
|
|
|
|
type CountCreatedIssueAssigneesParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
CreatorID pgtype.UUID `json:"creator_id"`
|
|
}
|
|
|
|
type CountCreatedIssueAssigneesRow struct {
|
|
AssigneeType pgtype.Text `json:"assignee_type"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
Frequency int64 `json:"frequency"`
|
|
}
|
|
|
|
// Count assignees on issues created by a specific user.
|
|
func (q *Queries) CountCreatedIssueAssignees(ctx context.Context, arg CountCreatedIssueAssigneesParams) ([]CountCreatedIssueAssigneesRow, error) {
|
|
rows, err := q.db.Query(ctx, countCreatedIssueAssignees, arg.WorkspaceID, arg.CreatorID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []CountCreatedIssueAssigneesRow{}
|
|
for rows.Next() {
|
|
var i CountCreatedIssueAssigneesRow
|
|
if err := rows.Scan(&i.AssigneeType, &i.AssigneeID, &i.Frequency); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const countIssues = `-- name: CountIssues :one
|
|
SELECT count(*) FROM issue
|
|
WHERE workspace_id = $1
|
|
AND ($2::text IS NULL OR status = $2)
|
|
AND ($3::text[] IS NULL OR priority = ANY($3::text[]))
|
|
AND ($4::text[] IS NULL OR assignee_type = ANY($4::text[]))
|
|
AND (
|
|
($5::uuid[] IS NULL AND NOT COALESCE($6::bool, false))
|
|
OR assignee_id = ANY($5::uuid[])
|
|
OR (COALESCE($6::bool, false) AND assignee_id IS NULL)
|
|
)
|
|
AND ($7::uuid[] IS NULL OR creator_id = ANY($7::uuid[]))
|
|
AND (
|
|
($8::uuid[] IS NULL AND NOT COALESCE($9::bool, false))
|
|
OR project_id = ANY($8::uuid[])
|
|
OR (COALESCE($9::bool, false) AND project_id IS NULL)
|
|
)
|
|
AND ($10::uuid[] IS NULL OR EXISTS (
|
|
SELECT 1 FROM issue_to_label il
|
|
WHERE il.issue_id = issue.id
|
|
AND il.label_id = ANY($10::uuid[])
|
|
))
|
|
`
|
|
|
|
type CountIssuesParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Status pgtype.Text `json:"status"`
|
|
Priorities []string `json:"priorities"`
|
|
AssigneeTypes []string `json:"assignee_types"`
|
|
AssigneeIds []pgtype.UUID `json:"assignee_ids"`
|
|
IncludeNoAssignee pgtype.Bool `json:"include_no_assignee"`
|
|
CreatorIds []pgtype.UUID `json:"creator_ids"`
|
|
ProjectIds []pgtype.UUID `json:"project_ids"`
|
|
IncludeNoProject pgtype.Bool `json:"include_no_project"`
|
|
LabelIds []pgtype.UUID `json:"label_ids"`
|
|
}
|
|
|
|
func (q *Queries) CountIssues(ctx context.Context, arg CountIssuesParams) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countIssues,
|
|
arg.WorkspaceID,
|
|
arg.Status,
|
|
arg.Priorities,
|
|
arg.AssigneeTypes,
|
|
arg.AssigneeIds,
|
|
arg.IncludeNoAssignee,
|
|
arg.CreatorIds,
|
|
arg.ProjectIds,
|
|
arg.IncludeNoProject,
|
|
arg.LabelIds,
|
|
)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const createIssue = `-- name: CreateIssue :one
|
|
INSERT INTO issue (
|
|
workspace_id, title, description, status, priority,
|
|
assignee_type, assignee_id, creator_type, creator_id,
|
|
parent_issue_id, position, due_date, number, project_id
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
|
) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at
|
|
`
|
|
|
|
type CreateIssueParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Title string `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
Status string `json:"status"`
|
|
Priority string `json:"priority"`
|
|
AssigneeType pgtype.Text `json:"assignee_type"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
CreatorType string `json:"creator_type"`
|
|
CreatorID pgtype.UUID `json:"creator_id"`
|
|
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
|
|
Position float64 `json:"position"`
|
|
DueDate pgtype.Timestamptz `json:"due_date"`
|
|
Number int32 `json:"number"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
}
|
|
|
|
func (q *Queries) CreateIssue(ctx context.Context, arg CreateIssueParams) (Issue, error) {
|
|
row := q.db.QueryRow(ctx, createIssue,
|
|
arg.WorkspaceID,
|
|
arg.Title,
|
|
arg.Description,
|
|
arg.Status,
|
|
arg.Priority,
|
|
arg.AssigneeType,
|
|
arg.AssigneeID,
|
|
arg.CreatorType,
|
|
arg.CreatorID,
|
|
arg.ParentIssueID,
|
|
arg.Position,
|
|
arg.DueDate,
|
|
arg.Number,
|
|
arg.ProjectID,
|
|
)
|
|
var i Issue
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createIssueWithOrigin = `-- name: CreateIssueWithOrigin :one
|
|
INSERT INTO issue (
|
|
workspace_id, title, description, status, priority,
|
|
assignee_type, assignee_id, creator_type, creator_id,
|
|
parent_issue_id, position, due_date, number, project_id,
|
|
origin_type, origin_id
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
|
|
$15, $16
|
|
) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at
|
|
`
|
|
|
|
type CreateIssueWithOriginParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Title string `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
Status string `json:"status"`
|
|
Priority string `json:"priority"`
|
|
AssigneeType pgtype.Text `json:"assignee_type"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
CreatorType string `json:"creator_type"`
|
|
CreatorID pgtype.UUID `json:"creator_id"`
|
|
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
|
|
Position float64 `json:"position"`
|
|
DueDate pgtype.Timestamptz `json:"due_date"`
|
|
Number int32 `json:"number"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
OriginType pgtype.Text `json:"origin_type"`
|
|
OriginID pgtype.UUID `json:"origin_id"`
|
|
}
|
|
|
|
func (q *Queries) CreateIssueWithOrigin(ctx context.Context, arg CreateIssueWithOriginParams) (Issue, error) {
|
|
row := q.db.QueryRow(ctx, createIssueWithOrigin,
|
|
arg.WorkspaceID,
|
|
arg.Title,
|
|
arg.Description,
|
|
arg.Status,
|
|
arg.Priority,
|
|
arg.AssigneeType,
|
|
arg.AssigneeID,
|
|
arg.CreatorType,
|
|
arg.CreatorID,
|
|
arg.ParentIssueID,
|
|
arg.Position,
|
|
arg.DueDate,
|
|
arg.Number,
|
|
arg.ProjectID,
|
|
arg.OriginType,
|
|
arg.OriginID,
|
|
)
|
|
var i Issue
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteIssue = `-- name: DeleteIssue :exec
|
|
DELETE FROM issue WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteIssue(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteIssue, id)
|
|
return err
|
|
}
|
|
|
|
const getIssue = `-- name: GetIssue :one
|
|
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at FROM issue
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetIssue(ctx context.Context, id pgtype.UUID) (Issue, error) {
|
|
row := q.db.QueryRow(ctx, getIssue, id)
|
|
var i Issue
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getIssueByNumber = `-- name: GetIssueByNumber :one
|
|
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at FROM issue
|
|
WHERE workspace_id = $1 AND number = $2
|
|
`
|
|
|
|
type GetIssueByNumberParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Number int32 `json:"number"`
|
|
}
|
|
|
|
func (q *Queries) GetIssueByNumber(ctx context.Context, arg GetIssueByNumberParams) (Issue, error) {
|
|
row := q.db.QueryRow(ctx, getIssueByNumber, arg.WorkspaceID, arg.Number)
|
|
var i Issue
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getIssueInWorkspace = `-- name: GetIssueInWorkspace :one
|
|
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at FROM issue
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetIssueInWorkspaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetIssueInWorkspace(ctx context.Context, arg GetIssueInWorkspaceParams) (Issue, error) {
|
|
row := q.db.QueryRow(ctx, getIssueInWorkspace, arg.ID, arg.WorkspaceID)
|
|
var i Issue
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listChildIssues = `-- name: ListChildIssues :many
|
|
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at FROM issue
|
|
WHERE parent_issue_id = $1
|
|
ORDER BY position ASC, created_at DESC
|
|
`
|
|
|
|
func (q *Queries) ListChildIssues(ctx context.Context, parentIssueID pgtype.UUID) ([]Issue, error) {
|
|
rows, err := q.db.Query(ctx, listChildIssues, parentIssueID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Issue{}
|
|
for rows.Next() {
|
|
var i Issue
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listIssues = `-- name: ListIssues :many
|
|
SELECT id, workspace_id, title, description, status, priority,
|
|
assignee_type, assignee_id, creator_type, creator_id,
|
|
parent_issue_id, position, due_date, created_at, updated_at, number, project_id
|
|
FROM issue
|
|
WHERE workspace_id = $1
|
|
AND ($4::text IS NULL OR status = $4)
|
|
AND ($5::text[] IS NULL OR priority = ANY($5::text[]))
|
|
AND ($6::text[] IS NULL OR assignee_type = ANY($6::text[]))
|
|
AND (
|
|
($7::uuid[] IS NULL AND NOT COALESCE($8::bool, false))
|
|
OR assignee_id = ANY($7::uuid[])
|
|
OR (COALESCE($8::bool, false) AND assignee_id IS NULL)
|
|
)
|
|
AND ($9::uuid[] IS NULL OR creator_id = ANY($9::uuid[]))
|
|
AND (
|
|
($10::uuid[] IS NULL AND NOT COALESCE($11::bool, false))
|
|
OR project_id = ANY($10::uuid[])
|
|
OR (COALESCE($11::bool, false) AND project_id IS NULL)
|
|
)
|
|
AND ($12::uuid[] IS NULL OR EXISTS (
|
|
SELECT 1 FROM issue_to_label il
|
|
WHERE il.issue_id = issue.id
|
|
AND il.label_id = ANY($12::uuid[])
|
|
))
|
|
ORDER BY position ASC, created_at DESC
|
|
LIMIT $2 OFFSET $3
|
|
`
|
|
|
|
type ListIssuesParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Limit int32 `json:"limit"`
|
|
Offset int32 `json:"offset"`
|
|
Status pgtype.Text `json:"status"`
|
|
Priorities []string `json:"priorities"`
|
|
AssigneeTypes []string `json:"assignee_types"`
|
|
AssigneeIds []pgtype.UUID `json:"assignee_ids"`
|
|
IncludeNoAssignee pgtype.Bool `json:"include_no_assignee"`
|
|
CreatorIds []pgtype.UUID `json:"creator_ids"`
|
|
ProjectIds []pgtype.UUID `json:"project_ids"`
|
|
IncludeNoProject pgtype.Bool `json:"include_no_project"`
|
|
LabelIds []pgtype.UUID `json:"label_ids"`
|
|
}
|
|
|
|
type ListIssuesRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Title string `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
Status string `json:"status"`
|
|
Priority string `json:"priority"`
|
|
AssigneeType pgtype.Text `json:"assignee_type"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
CreatorType string `json:"creator_type"`
|
|
CreatorID pgtype.UUID `json:"creator_id"`
|
|
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
|
|
Position float64 `json:"position"`
|
|
DueDate pgtype.Timestamptz `json:"due_date"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
Number int32 `json:"number"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
}
|
|
|
|
func (q *Queries) ListIssues(ctx context.Context, arg ListIssuesParams) ([]ListIssuesRow, error) {
|
|
rows, err := q.db.Query(ctx, listIssues,
|
|
arg.WorkspaceID,
|
|
arg.Limit,
|
|
arg.Offset,
|
|
arg.Status,
|
|
arg.Priorities,
|
|
arg.AssigneeTypes,
|
|
arg.AssigneeIds,
|
|
arg.IncludeNoAssignee,
|
|
arg.CreatorIds,
|
|
arg.ProjectIds,
|
|
arg.IncludeNoProject,
|
|
arg.LabelIds,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListIssuesRow{}
|
|
for rows.Next() {
|
|
var i ListIssuesRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listOpenIssues = `-- name: ListOpenIssues :many
|
|
SELECT id, workspace_id, title, description, status, priority,
|
|
assignee_type, assignee_id, creator_type, creator_id,
|
|
parent_issue_id, position, due_date, created_at, updated_at, number, project_id
|
|
FROM issue
|
|
WHERE workspace_id = $1
|
|
AND status NOT IN ('done', 'cancelled')
|
|
AND ($2::text[] IS NULL OR priority = ANY($2::text[]))
|
|
AND ($3::text[] IS NULL OR assignee_type = ANY($3::text[]))
|
|
AND (
|
|
($4::uuid[] IS NULL AND NOT COALESCE($5::bool, false))
|
|
OR assignee_id = ANY($4::uuid[])
|
|
OR (COALESCE($5::bool, false) AND assignee_id IS NULL)
|
|
)
|
|
AND ($6::uuid[] IS NULL OR creator_id = ANY($6::uuid[]))
|
|
AND (
|
|
($7::uuid[] IS NULL AND NOT COALESCE($8::bool, false))
|
|
OR project_id = ANY($7::uuid[])
|
|
OR (COALESCE($8::bool, false) AND project_id IS NULL)
|
|
)
|
|
AND ($9::uuid[] IS NULL OR EXISTS (
|
|
SELECT 1 FROM issue_to_label il
|
|
WHERE il.issue_id = issue.id
|
|
AND il.label_id = ANY($9::uuid[])
|
|
))
|
|
ORDER BY position ASC, created_at DESC
|
|
`
|
|
|
|
type ListOpenIssuesParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Priorities []string `json:"priorities"`
|
|
AssigneeTypes []string `json:"assignee_types"`
|
|
AssigneeIds []pgtype.UUID `json:"assignee_ids"`
|
|
IncludeNoAssignee pgtype.Bool `json:"include_no_assignee"`
|
|
CreatorIds []pgtype.UUID `json:"creator_ids"`
|
|
ProjectIds []pgtype.UUID `json:"project_ids"`
|
|
IncludeNoProject pgtype.Bool `json:"include_no_project"`
|
|
LabelIds []pgtype.UUID `json:"label_ids"`
|
|
}
|
|
|
|
type ListOpenIssuesRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Title string `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
Status string `json:"status"`
|
|
Priority string `json:"priority"`
|
|
AssigneeType pgtype.Text `json:"assignee_type"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
CreatorType string `json:"creator_type"`
|
|
CreatorID pgtype.UUID `json:"creator_id"`
|
|
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
|
|
Position float64 `json:"position"`
|
|
DueDate pgtype.Timestamptz `json:"due_date"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
Number int32 `json:"number"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
}
|
|
|
|
func (q *Queries) ListOpenIssues(ctx context.Context, arg ListOpenIssuesParams) ([]ListOpenIssuesRow, error) {
|
|
rows, err := q.db.Query(ctx, listOpenIssues,
|
|
arg.WorkspaceID,
|
|
arg.Priorities,
|
|
arg.AssigneeTypes,
|
|
arg.AssigneeIds,
|
|
arg.IncludeNoAssignee,
|
|
arg.CreatorIds,
|
|
arg.ProjectIds,
|
|
arg.IncludeNoProject,
|
|
arg.LabelIds,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListOpenIssuesRow{}
|
|
for rows.Next() {
|
|
var i ListOpenIssuesRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const markIssueFirstExecuted = `-- name: MarkIssueFirstExecuted :one
|
|
|
|
UPDATE issue
|
|
SET first_executed_at = now()
|
|
WHERE id = $1 AND first_executed_at IS NULL
|
|
RETURNING id, workspace_id, creator_type, creator_id, first_executed_at
|
|
`
|
|
|
|
type MarkIssueFirstExecutedRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
CreatorType string `json:"creator_type"`
|
|
CreatorID pgtype.UUID `json:"creator_id"`
|
|
FirstExecutedAt pgtype.Timestamptz `json:"first_executed_at"`
|
|
}
|
|
|
|
// SearchIssues: moved to handler (dynamic SQL for multi-word search support).
|
|
// Flips first_executed_at from NULL to now() atomically. Returns the row if
|
|
// this was the first time the issue was executed; no rows otherwise. The
|
|
// analytics issue_executed event fires exactly when this returns a row —
|
|
// retries and re-assignments hit the WHERE clause and no-op.
|
|
func (q *Queries) MarkIssueFirstExecuted(ctx context.Context, id pgtype.UUID) (MarkIssueFirstExecutedRow, error) {
|
|
row := q.db.QueryRow(ctx, markIssueFirstExecuted, id)
|
|
var i MarkIssueFirstExecutedRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.FirstExecutedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateIssue = `-- name: UpdateIssue :one
|
|
UPDATE issue SET
|
|
title = COALESCE($2, title),
|
|
description = COALESCE($3, description),
|
|
status = COALESCE($4, status),
|
|
priority = COALESCE($5, priority),
|
|
assignee_type = $6,
|
|
assignee_id = $7,
|
|
position = COALESCE($8, position),
|
|
due_date = $9,
|
|
parent_issue_id = $10,
|
|
project_id = $11,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at
|
|
`
|
|
|
|
type UpdateIssueParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Title pgtype.Text `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
Status pgtype.Text `json:"status"`
|
|
Priority pgtype.Text `json:"priority"`
|
|
AssigneeType pgtype.Text `json:"assignee_type"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
Position pgtype.Float8 `json:"position"`
|
|
DueDate pgtype.Timestamptz `json:"due_date"`
|
|
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
}
|
|
|
|
func (q *Queries) UpdateIssue(ctx context.Context, arg UpdateIssueParams) (Issue, error) {
|
|
row := q.db.QueryRow(ctx, updateIssue,
|
|
arg.ID,
|
|
arg.Title,
|
|
arg.Description,
|
|
arg.Status,
|
|
arg.Priority,
|
|
arg.AssigneeType,
|
|
arg.AssigneeID,
|
|
arg.Position,
|
|
arg.DueDate,
|
|
arg.ParentIssueID,
|
|
arg.ProjectID,
|
|
)
|
|
var i Issue
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateIssueStatus = `-- name: UpdateIssueStatus :one
|
|
UPDATE issue SET
|
|
status = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at
|
|
`
|
|
|
|
type UpdateIssueStatusParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (q *Queries) UpdateIssueStatus(ctx context.Context, arg UpdateIssueStatusParams) (Issue, error) {
|
|
row := q.db.QueryRow(ctx, updateIssueStatus, arg.ID, arg.Status)
|
|
var i Issue
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
)
|
|
return i, err
|
|
}
|