mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
* feat(search): implement full-text search for issues Add pg_bigm-based full-text search across issue titles and descriptions, with API endpoint, CLI subcommand, and web Cmd+K search dialog. - Migration 032: pg_bigm extension + GIN indexes on title/description - Server: GET /api/issues/search?q=... with pagination and total count - CLI: `multica issue search <query>` with table/json output - Web: Cmd+K command palette using cmdk, with debounced search Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(search): address review feedback on search implementation 1. Escape LIKE special characters (%, _, \) in handler to prevent matching anomalies from user input. 2. Wire AbortController signal into searchIssues fetch so in-flight requests are actually cancelled on new input. 3. Fix offset=0 falsy check — use !== undefined instead of truthiness. 4. Merge results + count into single query using COUNT(*) OVER() window function, eliminating the duplicate DB round-trip. 5. Exclude done/cancelled issues by default; add include_closed parameter to API, CLI (--include-closed), and web client. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(search): default web search to include all statuses Pass include_closed: true in the web Cmd+K search so results include done and cancelled issues by default, matching the reviewer's request. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(search): add comment search with snippet extraction Extend search to cover issue comments in addition to title/description. Results are deduplicated at the issue level, with match_source and matched_snippet fields indicating where and what matched. - Migration 033: pg_bigm GIN index on comment.content - SQL: EXISTS subquery for comment matching, correlated subquery for snippet extraction, 3-tier ranking (title > description > comment) - Server: SearchIssueResponse with match_source and matched_snippet - Web: show comment icon + snippet below issue title when matched - CLI: MATCH column shows source and truncated snippet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(search): redesign search dialog to match Linear's spacious style - Widen dialog from sm (384px) to xl (576px) with top-20% positioning - Larger search input with icon, generous padding, and ESC hint - Use cmdk primitives directly for full style control - Taller result list (400px / 50vh), spacious result items (py-2.5) - Rounded-lg items with accent highlight on selection - Cleaner border separator between input and results --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
113 lines
3.5 KiB
SQL
113 lines
3.5 KiB
SQL
-- name: ListIssues :many
|
|
SELECT * FROM issue
|
|
WHERE workspace_id = $1
|
|
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
|
|
AND (sqlc.narg('priority')::text IS NULL OR priority = sqlc.narg('priority'))
|
|
AND (sqlc.narg('assignee_id')::uuid IS NULL OR assignee_id = sqlc.narg('assignee_id'))
|
|
ORDER BY position ASC, created_at DESC
|
|
LIMIT $2 OFFSET $3;
|
|
|
|
-- name: GetIssue :one
|
|
SELECT * FROM issue
|
|
WHERE id = $1;
|
|
|
|
-- name: GetIssueInWorkspace :one
|
|
SELECT * FROM issue
|
|
WHERE id = $1 AND workspace_id = $2;
|
|
|
|
-- 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
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
|
|
) RETURNING *;
|
|
|
|
-- name: GetIssueByNumber :one
|
|
SELECT * FROM issue
|
|
WHERE workspace_id = $1 AND number = $2;
|
|
|
|
-- name: UpdateIssue :one
|
|
UPDATE issue SET
|
|
title = COALESCE(sqlc.narg('title'), title),
|
|
description = COALESCE(sqlc.narg('description'), description),
|
|
status = COALESCE(sqlc.narg('status'), status),
|
|
priority = COALESCE(sqlc.narg('priority'), priority),
|
|
assignee_type = sqlc.narg('assignee_type'),
|
|
assignee_id = sqlc.narg('assignee_id'),
|
|
position = COALESCE(sqlc.narg('position'), position),
|
|
due_date = sqlc.narg('due_date'),
|
|
parent_issue_id = sqlc.narg('parent_issue_id'),
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: UpdateIssueStatus :one
|
|
UPDATE issue SET
|
|
status = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: DeleteIssue :exec
|
|
DELETE FROM issue WHERE id = $1;
|
|
|
|
-- name: ListOpenIssues :many
|
|
SELECT * FROM issue
|
|
WHERE workspace_id = $1
|
|
AND status NOT IN ('done', 'cancelled')
|
|
AND (sqlc.narg('priority')::text IS NULL OR priority = sqlc.narg('priority'))
|
|
AND (sqlc.narg('assignee_id')::uuid IS NULL OR assignee_id = sqlc.narg('assignee_id'))
|
|
ORDER BY position ASC, created_at DESC;
|
|
|
|
-- name: CountIssues :one
|
|
SELECT count(*) FROM issue
|
|
WHERE workspace_id = $1
|
|
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
|
|
AND (sqlc.narg('priority')::text IS NULL OR priority = sqlc.narg('priority'))
|
|
AND (sqlc.narg('assignee_id')::uuid IS NULL OR assignee_id = sqlc.narg('assignee_id'));
|
|
|
|
-- name: ListChildIssues :many
|
|
SELECT * FROM issue
|
|
WHERE parent_issue_id = $1
|
|
ORDER BY position ASC, created_at DESC;
|
|
|
|
-- name: SearchIssues :many
|
|
SELECT i.*,
|
|
COUNT(*) OVER() AS total_count,
|
|
CASE
|
|
WHEN i.title LIKE '%' || @query || '%' THEN 'title'
|
|
WHEN COALESCE(i.description, '') LIKE '%' || @query || '%' THEN 'description'
|
|
ELSE 'comment'
|
|
END AS match_source,
|
|
CASE
|
|
WHEN i.title LIKE '%' || @query || '%' THEN ''
|
|
WHEN COALESCE(i.description, '') LIKE '%' || @query || '%' THEN ''
|
|
ELSE COALESCE(
|
|
(SELECT c.content FROM comment c
|
|
WHERE c.issue_id = i.id AND c.content LIKE '%' || @query || '%'
|
|
ORDER BY c.created_at DESC LIMIT 1),
|
|
''
|
|
)
|
|
END AS matched_comment_content
|
|
FROM issue i
|
|
WHERE i.workspace_id = @workspace_id
|
|
AND (
|
|
i.title LIKE '%' || @query || '%'
|
|
OR COALESCE(i.description, '') LIKE '%' || @query || '%'
|
|
OR EXISTS (
|
|
SELECT 1 FROM comment c
|
|
WHERE c.issue_id = i.id AND c.content LIKE '%' || @query || '%'
|
|
)
|
|
)
|
|
AND (@include_closed::boolean OR i.status NOT IN ('done', 'cancelled'))
|
|
ORDER BY
|
|
CASE
|
|
WHEN i.title LIKE '%' || @query || '%' THEN 0
|
|
WHEN COALESCE(i.description, '') LIKE '%' || @query || '%' THEN 1
|
|
ELSE 2
|
|
END,
|
|
i.updated_at DESC
|
|
LIMIT @search_limit OFFSET @search_offset;
|