Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 33 additions & 14 deletions internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,28 @@ import (

// --- Response types ---

// PendingItemJSON is a summary of a pending upstream PR for browse list hover cards.
type PendingItemJSON struct {
RigHandle string `json:"rig_handle"`
Status string `json:"status,omitempty"`
PRURL string `json:"pr_url,omitempty"`
BranchURL string `json:"branch_url,omitempty"`
}

// WantedSummaryJSON is the JSON representation of a browse list item.
type WantedSummaryJSON struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Project string `json:"project,omitempty"`
Type string `json:"type,omitempty"`
Priority int `json:"priority"`
PostedBy string `json:"posted_by,omitempty"`
ClaimedBy string `json:"claimed_by,omitempty"`
Status string `json:"status"`
EffortLevel string `json:"effort_level"`
PendingCount int `json:"pending_count,omitempty"`
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Project string `json:"project,omitempty"`
Type string `json:"type,omitempty"`
Priority int `json:"priority"`
PostedBy string `json:"posted_by,omitempty"`
ClaimedBy string `json:"claimed_by,omitempty"`
Status string `json:"status"`
EffortLevel string `json:"effort_level"`
PendingCount int `json:"pending_count,omitempty"`
PendingItems []PendingItemJSON `json:"pending_items,omitempty"`
}

// BrowseResponse is the JSON response for GET /api/wanted.
Expand Down Expand Up @@ -335,7 +344,16 @@ func toMutationResponse(r *sdk.MutationResult, mode string) *MutationResponse {
}
}

func toSummaryJSON(s commons.WantedSummary, pendingCount int) WantedSummaryJSON {
func toSummaryJSON(s commons.WantedSummary, pendingCount int, pending []sdk.PendingItem) WantedSummaryJSON {
var pendingItems []PendingItemJSON
for _, p := range pending {
pendingItems = append(pendingItems, PendingItemJSON{
RigHandle: p.RigHandle,
Status: p.Status,
PRURL: p.PRURL,
BranchURL: p.BranchURL,
})
}
return WantedSummaryJSON{
ID: s.ID,
Title: s.Title,
Expand All @@ -348,13 +366,14 @@ func toSummaryJSON(s commons.WantedSummary, pendingCount int) WantedSummaryJSON
Status: s.Status,
EffortLevel: s.EffortLevel,
PendingCount: pendingCount,
PendingItems: pendingItems,
}
}

func toBrowseResponse(r *sdk.BrowseResult) *BrowseResponse {
items := make([]WantedSummaryJSON, len(r.Items))
for i, s := range r.Items {
items[i] = toSummaryJSON(s, r.PendingIDs[s.ID])
items[i] = toSummaryJSON(s, r.PendingIDs[s.ID], r.UpstreamPending[s.ID])
}
return &BrowseResponse{Items: items}
}
Expand All @@ -378,7 +397,7 @@ func toDashboardResponse(d *commons.DashboardData) *DashboardResponse {
convert := func(items []commons.WantedSummary) []WantedSummaryJSON {
result := make([]WantedSummaryJSON, len(items))
for i, s := range items {
result[i] = toSummaryJSON(s, 0)
result[i] = toSummaryJSON(s, 0, nil)
}
return result
}
Expand Down
6 changes: 6 additions & 0 deletions internal/backend/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,12 @@ func (r *RemoteDB) pollOperation(operationName string) error {

body, err := r.doGet(apiURL)
if err != nil {
// DoltHub returns HTTP 400 with toCommitId null when the write
// produced no changes (e.g. ON DUPLICATE KEY UPDATE with same
// values). Treat this as a no-op success.
if strings.Contains(strings.ToLower(err.Error()), "sqlwrite.tocommitid") {
return nil
}
lastErr = err
consecutiveErrors++
// Fail fast: if every poll attempt errors, don't wait the full 2 minutes.
Expand Down
4 changes: 0 additions & 4 deletions internal/sdk/reads.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,6 @@ func (c *Client) Browse(filter commons.BrowseFilter) (*BrowseResult, error) {
best = p
}
}
// Only overlay status if upstream state is further than current.
if stateRank[best.Status] > stateRank[items[i].Status] {
items[i].Status = best.Status
}
// Overlay claimed_by to reflect the full set of candidates
// (main claimer + upstream PRs).
totalCandidates := len(pending)
Expand Down
12 changes: 6 additions & 6 deletions internal/sdk/sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -800,9 +800,9 @@ func TestBrowse_PendingClaimedBy_Single(t *testing.T) {
if item.ClaimedBy != "charlie (pending)" {
t.Errorf("w-1: expected ClaimedBy='charlie (pending)', got %q", item.ClaimedBy)
}
// Status should be promoted from "open" to "claimed" to match the pending claim.
if item.Status != "claimed" {
t.Errorf("w-1: expected Status='claimed', got %q", item.Status)
// Status reflects the raw DB value; pending state is conveyed via ClaimedBy and PendingIDs.
if item.Status != "open" {
t.Errorf("w-1: expected Status='open' (raw DB), got %q", item.Status)
}
case "w-2":
// Already claimed on main — should not be overwritten.
Expand Down Expand Up @@ -838,9 +838,9 @@ func TestBrowse_PendingFurthestState(t *testing.T) {

for _, item := range result.Items {
if item.ID == "w-1" {
// Furthest state is "in_review" from dave.
if item.Status != "in_review" {
t.Errorf("w-1: expected Status='in_review' (furthest), got %q", item.Status)
// Status stays as raw DB value; pending state is conveyed via ClaimedBy and PendingIDs.
if item.Status != "open" {
t.Errorf("w-1: expected Status='open' (raw DB), got %q", item.Status)
}
if item.ClaimedBy != "Multiple (pending)" {
t.Errorf("w-1: expected ClaimedBy='Multiple (pending)', got %q", item.ClaimedBy)
Expand Down
Empty file removed web/dist/.gitkeep
Empty file.
8 changes: 8 additions & 0 deletions web/src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export interface PendingItemSummary {
rig_handle: string;
status?: string;
pr_url?: string;
branch_url?: string;
}

export interface WantedSummary {
id: string;
title: string;
Expand All @@ -9,6 +16,7 @@ export interface WantedSummary {
status: string;
effort_level: string;
pending_count?: number;
pending_items?: PendingItemSummary[];
}

export interface BrowseResponse {
Expand Down
63 changes: 63 additions & 0 deletions web/src/components/BrowseList.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,69 @@
font-family: var(--font-heading);
letter-spacing: 0.04em;
text-transform: uppercase;
position: relative;
cursor: default;
}

.pendingIndicator:hover .pendingCard {
display: block;
}

.pendingCard {
display: none;
position: absolute;
bottom: calc(100% + 6px);
left: 0;
z-index: 100;
min-width: 220px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: var(--space-2) var(--space-3);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
font-family: var(--font-body);
font-size: var(--text-sm);
font-weight: 400;
letter-spacing: 0;
text-transform: none;
white-space: nowrap;
color: var(--fg);
}

.pendingCardTitle {
color: var(--dim);
font-size: var(--text-xs);
font-family: var(--font-heading);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: var(--space-1);
}

.pendingCardRow {
display: flex;
align-items: center;
gap: var(--space-2);
padding: 2px 0;
}

.pendingCardHandle {
color: var(--brass);
font-weight: 600;
}

.pendingCardStatus {
color: var(--dim);
font-size: var(--text-xs);
}

.pendingCardLink {
color: var(--blue, var(--dim));
font-size: var(--text-xs);
text-decoration: none;
}

.pendingCardLink:hover {
text-decoration: underline;
}

.pendingCount {
Expand Down
46 changes: 33 additions & 13 deletions web/src/components/BrowseList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Link, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { browse } from "../api/client";
import { consumePrefetch } from "../api/prefetch";
import type { WantedSummary } from "../api/types";
import type { PendingItemSummary, WantedSummary } from "../api/types";
import { useFilterParams } from "../hooks/useFilterParams";
import styles from "./BrowseList.module.css";
import { EmptyState } from "./EmptyState";
Expand Down Expand Up @@ -169,12 +169,7 @@ export function BrowseList() {
<span className={styles.statusCell}>
<StatusBadge status={item.status} />
{item.pending_count != null && item.pending_count > 0 && (
<span className={styles.pendingIndicator}>
pending
{item.pending_count > 1 && (
<span className={styles.pendingCount}>&times;{item.pending_count}</span>
)}
</span>
<PendingIndicator count={item.pending_count} items={item.pending_items} />
)}
</span>
</td>
Expand All @@ -193,12 +188,7 @@ export function BrowseList() {
<PriorityBadge priority={item.priority} />
<StatusBadge status={item.status} />
{item.pending_count != null && item.pending_count > 0 && (
<span className={styles.pendingIndicator}>
pending
{item.pending_count > 1 && (
<span className={styles.pendingCount}>&times;{item.pending_count}</span>
)}
</span>
<PendingIndicator count={item.pending_count} items={item.pending_items} />
)}
</div>
<Link to={`/wanted/${item.id}`} className={styles.cardTitle}>
Expand Down Expand Up @@ -237,3 +227,33 @@ export function BrowseList() {
</div>
);
}

function PendingIndicator({ count, items }: { count: number; items?: PendingItemSummary[] }) {
return (
<span className={styles.pendingIndicator}>
pending
{count > 1 && <span className={styles.pendingCount}>&times;{count}</span>}
{items && items.length > 0 && (
<span className={styles.pendingCard}>
<div className={styles.pendingCardTitle}>Competing submissions</div>
{items.map((p, i) => (
<div key={i} className={styles.pendingCardRow}>
<span className={styles.pendingCardHandle}>{p.rig_handle}</span>
{p.status && <span className={styles.pendingCardStatus}>{p.status.replace("_", " ")}</span>}
{p.pr_url && (
<a href={p.pr_url} target="_blank" rel="noopener noreferrer" className={styles.pendingCardLink}>
PR
</a>
)}
{p.branch_url && !p.pr_url && (
<a href={p.branch_url} target="_blank" rel="noopener noreferrer" className={styles.pendingCardLink}>
branch
</a>
)}
</div>
))}
</span>
)}
</span>
);
}
1 change: 1 addition & 0 deletions web/tsconfig.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/test-setup.ts","./src/test-utils.tsx","./src/vite-env.d.ts","./src/api/client.test.ts","./src/api/client.ts","./src/api/nango.ts","./src/api/prefetch.ts","./src/api/types.ts","./src/components/ActionButton.test.tsx","./src/components/ActionButton.tsx","./src/components/BrowseList.test.tsx","./src/components/BrowseList.tsx","./src/components/CommandPalette.tsx","./src/components/ConfirmDialog.test.tsx","./src/components/ConfirmDialog.tsx","./src/components/ConnectPage.tsx","./src/components/Dashboard.test.tsx","./src/components/Dashboard.tsx","./src/components/DetailView.test.tsx","./src/components/DetailView.tsx","./src/components/EmptyState.test.tsx","./src/components/EmptyState.tsx","./src/components/FilterBar.test.tsx","./src/components/FilterBar.tsx","./src/components/Layout.tsx","./src/components/PriorityBadge.test.tsx","./src/components/PriorityBadge.tsx","./src/components/ProfileSearch.tsx","./src/components/ProfileView.tsx","./src/components/Scoreboard.test.tsx","./src/components/Scoreboard.tsx","./src/components/Settings.test.tsx","./src/components/Settings.tsx","./src/components/ShortcutHelp.tsx","./src/components/Skeleton.tsx","./src/components/StatusBadge.test.tsx","./src/components/StatusBadge.tsx","./src/components/Toaster.tsx","./src/components/WantedForm.test.tsx","./src/components/WantedForm.tsx","./src/context/WastelandContext.tsx","./src/hooks/useCommands.test.ts","./src/hooks/useCommands.ts","./src/hooks/useFilterParams.test.tsx","./src/hooks/useFilterParams.ts","./src/hooks/useFocusTrap.test.tsx","./src/hooks/useFocusTrap.ts","./src/hooks/useGlobalShortcuts.test.tsx","./src/hooks/useGlobalShortcuts.ts","./src/styles/theme.ts"],"version":"5.7.3"}