Detailed changes
@@ -19,7 +19,6 @@ import (
type RepositoryResolver interface {
Name(ctx context.Context, obj *models.Repository) (*string, error)
- LocalName(ctx context.Context, obj *models.Repository) (string, error)
AllBugs(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int, query *string) (*models.BugConnection, error)
Bug(ctx context.Context, obj *models.Repository, prefix string) (models.BugWrapper, error)
AllIdentities(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int) (*models.IdentityConnection, error)
@@ -451,50 +450,6 @@ func (ec *executionContext) fieldContext_Repository_name(_ context.Context, fiel
return fc, nil
}
-func (ec *executionContext) _Repository_localName(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
- fc, err := ec.fieldContext_Repository_localName(ctx, field)
- if err != nil {
- return graphql.Null
- }
- ctx = graphql.WithFieldContext(ctx, fc)
- defer func() {
- if r := recover(); r != nil {
- ec.Error(ctx, ec.Recover(ctx, r))
- ret = graphql.Null
- }
- }()
- resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
- ctx = rctx // use context from middleware stack in children
- return ec.resolvers.Repository().LocalName(rctx, obj)
- })
- if err != nil {
- ec.Error(ctx, err)
- return graphql.Null
- }
- if resTmp == nil {
- if !graphql.HasFieldError(ctx, fc) {
- ec.Errorf(ctx, "must not be null")
- }
- return graphql.Null
- }
- res := resTmp.(string)
- fc.Result = res
- return ec.marshalNString2string(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_Repository_localName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
- fc = &graphql.FieldContext{
- Object: "Repository",
- Field: field,
- IsMethod: true,
- IsResolver: true,
- Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
- return nil, errors.New("field of type String does not have child fields")
- },
- }
- return fc, nil
-}
-
func (ec *executionContext) _Repository_allBugs(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Repository_allBugs(ctx, field)
if err != nil {
@@ -990,8 +945,6 @@ func (ec *executionContext) fieldContext_RepositoryConnection_nodes(_ context.Co
switch field.Name {
case "name":
return ec.fieldContext_Repository_name(ctx, field)
- case "localName":
- return ec.fieldContext_Repository_localName(ctx, field)
case "allBugs":
return ec.fieldContext_Repository_allBugs(ctx, field)
case "bug":
@@ -1194,8 +1147,6 @@ func (ec *executionContext) fieldContext_RepositoryEdge_node(_ context.Context,
switch field.Name {
case "name":
return ec.fieldContext_Repository_name(ctx, field)
- case "localName":
- return ec.fieldContext_Repository_localName(ctx, field)
case "allBugs":
return ec.fieldContext_Repository_allBugs(ctx, field)
case "bug":
@@ -1270,42 +1221,6 @@ func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSe
continue
}
- out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
- case "localName":
- field := field
-
- innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
- defer func() {
- if r := recover(); r != nil {
- ec.Error(ctx, ec.Recover(ctx, r))
- }
- }()
- res = ec._Repository_localName(ctx, field, obj)
- if res == graphql.Null {
- atomic.AddUint32(&fs.Invalids, 1)
- }
- return res
- }
-
- if field.Deferrable != nil {
- dfs, ok := deferred[field.Deferrable.Label]
- di := 0
- if ok {
- dfs.AddField(field)
- di = len(dfs.Values) - 1
- } else {
- dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
- deferred[field.Deferrable.Label] = dfs
- }
- dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
- return innerFunc(ctx, dfs)
- })
-
- // don't run the out.Concurrently() call below
- out.Values[i] = graphql.Null
- continue
- }
-
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "allBugs":
field := field
@@ -1112,8 +1112,6 @@ func (ec *executionContext) fieldContext_Query_repository(ctx context.Context, f
switch field.Name {
case "name":
return ec.fieldContext_Repository_name(ctx, field)
- case "localName":
- return ec.fieldContext_Repository_localName(ctx, field)
case "allBugs":
return ec.fieldContext_Repository_allBugs(ctx, field)
case "bug":
@@ -385,7 +385,6 @@ type ComplexityRoot struct {
AllIdentities func(childComplexity int, after *string, before *string, first *int, last *int) int
Bug func(childComplexity int, prefix string) int
Identity func(childComplexity int, prefix string) int
- LocalName func(childComplexity int) int
Name func(childComplexity int) int
UserIdentity func(childComplexity int) int
ValidLabels func(childComplexity int, after *string, before *string, first *int, last *int) int
@@ -1857,13 +1856,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Repository.Identity(childComplexity, args["prefix"].(string)), true
- case "Repository.localName":
- if e.complexity.Repository.LocalName == nil {
- break
- }
-
- return e.complexity.Repository.LocalName(childComplexity), true
-
case "Repository.name":
if e.complexity.Repository.Name == nil {
break
@@ -2736,9 +2728,6 @@ type OperationEdge {
"""The name of the repository. Null for the default (unnamed) repository."""
name: String
- """The local directory name of the repository (basename only, no path)."""
- localName: String!
-
"""All the bugs"""
allBugs(
"""Returns the elements in the list that come after the specified cursor."""
@@ -2,9 +2,6 @@ type Repository {
"""The name of the repository. Null for the default (unnamed) repository."""
name: String
- """The local directory name of the repository (basename only, no path)."""
- localName: String!
-
"""All the bugs"""
allBugs(
"""Returns the elements in the list that come after the specified cursor."""
@@ -1,5 +1,5 @@
import { useState } from 'react'
-import { ChevronDown, Tag, User, X, Search, Check } from 'lucide-react'
+import { ArrowUpDown, ChevronDown, Tag, User, X, Search, Check } from 'lucide-react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { LabelBadge } from './LabelBadge'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
@@ -26,6 +26,15 @@ function authorQueryValue(i: { login?: string | null; name?: string | null; huma
return i.login || i.name || i.humanId
}
+export type SortValue = 'creation-desc' | 'creation-asc' | 'edit-desc' | 'edit-asc'
+
+const SORT_OPTIONS: { value: SortValue; label: string }[] = [
+ { value: 'creation-desc', label: 'Newest' },
+ { value: 'creation-asc', label: 'Oldest' },
+ { value: 'edit-desc', label: 'Recently updated' },
+ { value: 'edit-asc', label: 'Least recently updated' },
+]
+
interface IssueFiltersProps {
selectedLabels: string[]
onLabelsChange: (labels: string[]) => void
@@ -33,6 +42,8 @@ interface IssueFiltersProps {
onAuthorChange: (humanId: string | null, queryValue: string | null) => void
/** humanIds of authors appearing in the current bug list, used to rank the initial suggestions */
recentAuthorIds?: string[]
+ sort: SortValue
+ onSortChange: (sort: SortValue) => void
}
// Label and author filter dropdowns shown in the issue list header bar.
@@ -52,6 +63,8 @@ export function IssueFilters({
selectedAuthorId,
onAuthorChange,
recentAuthorIds = [],
+ sort,
+ onSortChange,
}: IssueFiltersProps) {
const { user } = useAuth()
const repo = useRepo()
@@ -135,7 +148,7 @@ export function IssueFilters({
const selectedAuthorIdentity = allIdentities.find((i) => i.humanId === selectedAuthorId)
return (
- <div className="flex items-center gap-1">
+ <div className="flex shrink-0 items-center gap-1">
{/* Label filter */}
<Popover onOpenChange={(open) => { if (!open) setLabelSearch('') }}>
<PopoverTrigger asChild>
@@ -298,6 +311,36 @@ export function IssueFilters({
)}
</PopoverContent>
</Popover>
+
+ {/* Sort */}
+ <Popover>
+ <PopoverTrigger asChild>
+ <button
+ className={cn(
+ 'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors whitespace-nowrap',
+ sort !== 'creation-desc'
+ ? 'bg-accent text-accent-foreground'
+ : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
+ )}
+ >
+ <ArrowUpDown className="size-3.5" />
+ {SORT_OPTIONS.find((o) => o.value === sort)?.label ?? 'Sort'}
+ <ChevronDown className="size-3" />
+ </button>
+ </PopoverTrigger>
+ <PopoverContent align="end" className="w-56 p-1 bg-popover shadow-lg">
+ {SORT_OPTIONS.map((opt) => (
+ <button
+ key={opt.value}
+ onClick={() => onSortChange(opt.value)}
+ className="flex w-full items-center gap-2 whitespace-nowrap rounded px-2 py-1.5 text-sm hover:bg-muted"
+ >
+ {opt.label}
+ {sort === opt.value && <Check className="ml-auto size-3.5 shrink-0 text-foreground" />}
+ </button>
+ ))}
+ </PopoverContent>
+ </Popover>
</div>
)
}
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { BugRow } from '@/components/bugs/BugRow'
import { IssueFilters } from '@/components/bugs/IssueFilters'
+import type { SortValue } from '@/components/bugs/IssueFilters'
import { QueryInput } from '@/components/bugs/QueryInput'
import { useBugListQuery } from '@/__generated__/graphql'
import { cn } from '@/lib/utils'
@@ -24,7 +25,8 @@ export function BugListPage() {
// query value (login/name) β what goes into author:... in the query string
const [selectedAuthorQuery, setSelectedAuthorQuery] = useState<string | null>(null)
const [freeText, setFreeText] = useState('')
- const [draft, setDraft] = useState(() => buildQueryString('open', [], null, ''))
+ const [sort, setSort] = useState<SortValue>('creation-desc')
+ const [draft, setDraft] = useState(() => buildQueryString('open', [], null, '', 'creation-desc'))
// Cursor-stack pagination: cursors[i] is the `after` value to fetch page i.
// cursors[0] is always undefined (first page needs no cursor).
@@ -36,7 +38,7 @@ export function BugListPage() {
const baseQuery = buildBaseQuery(selectedLabels, selectedAuthorQuery, freeText)
const openQuery = `status:open ${baseQuery}`.trim()
const closedQuery = `status:closed ${baseQuery}`.trim()
- const listQuery = buildQueryString(statusFilter, selectedLabels, selectedAuthorQuery, freeText)
+ const listQuery = buildQueryString(statusFilter, selectedLabels, selectedAuthorQuery, freeText, sort)
const { data, loading, error } = useBugListQuery({
variables: { ref: repo, openQuery, closedQuery, listQuery, first: PAGE_SIZE, after: cursors[page] },
@@ -60,13 +62,15 @@ export function BugListPage() {
authorId: string | null,
authorQuery: string | null,
text: string,
+ sortVal: SortValue = sort,
) {
setStatusFilter(status)
setSelectedLabels(labels)
setSelectedAuthorId(authorId)
setSelectedAuthorQuery(authorQuery)
setFreeText(text)
- setDraft(buildQueryString(status, labels, authorQuery, text))
+ setSort(sortVal)
+ setDraft(buildQueryString(status, labels, authorQuery, text, sortVal))
}
// Parse the draft text box on submit so manual edits update the dropdowns too.
@@ -76,7 +80,7 @@ export function BugListPage() {
function handleSearch(e?: React.FormEvent) {
e?.preventDefault()
const p = parseQueryString(draft)
- applyFilters(p.status, p.labels, null, p.author, p.freeText)
+ applyFilters(p.status, p.labels, null, p.author, p.freeText, p.sort)
}
function goNext() {
@@ -107,8 +111,8 @@ export function BugListPage() {
{/* List container */}
<div className="rounded-md border border-border">
{/* Open / Closed toggle + filter dropdowns */}
- <div className="flex items-center border-b border-border px-4 py-2">
- <div className="flex items-center gap-1">
+ <div className="flex items-center gap-2 overflow-x-auto border-b border-border px-4 py-2">
+ <div className="flex shrink-0 items-center gap-1">
<button
onClick={() => applyFilters('open', selectedLabels, selectedAuthorId, selectedAuthorQuery, freeText)}
className={cn(
@@ -149,6 +153,8 @@ export function BugListPage() {
selectedAuthorId={selectedAuthorId}
onAuthorChange={(id, qv) => applyFilters(statusFilter, selectedLabels, id, qv, freeText)}
recentAuthorIds={bugs?.nodes?.map((b) => b.author.humanId) ?? []}
+ sort={sort}
+ onSortChange={(s) => applyFilters(statusFilter, selectedLabels, selectedAuthorId, selectedAuthorQuery, freeText, s)}
/>
</div>
</div>
@@ -242,8 +248,13 @@ function buildQueryString(
labels: string[],
author: string | null,
freeText: string,
+ sort: SortValue = 'creation-desc',
): string {
- return `status:${status} ${buildBaseQuery(labels, author, freeText)}`.trim()
+ const parts = [`status:${status}`]
+ const base = buildBaseQuery(labels, author, freeText)
+ if (base) parts.push(base)
+ if (sort !== 'creation-desc') parts.push(`sort:${sort}`)
+ return parts.join(' ')
}
// Tokenize a query string, keeping quoted spans (e.g. author:"RenΓ© Descartes")
@@ -266,15 +277,19 @@ function tokenizeQuery(input: string): string[] {
// manual edits to the search box are reflected in the dropdown UI on submit.
// Strips surrounding quotes from values (they're an encoding detail, not part
// of the value itself). Unknown tokens fall through to freeText.
+const VALID_SORTS = new Set<SortValue>(['creation-desc', 'creation-asc', 'edit-desc', 'edit-asc'])
+
function parseQueryString(input: string): {
status: StatusFilter
labels: string[]
author: string | null
freeText: string
+ sort: SortValue
} {
let status: StatusFilter = 'open'
const labels: string[] = []
let author: string | null = null
+ let sort: SortValue = 'creation-desc'
const free: string[] = []
for (const token of tokenizeQuery(input)) {
@@ -282,10 +297,14 @@ function parseQueryString(input: string): {
else if (token === 'status:closed') status = 'closed'
else if (token.startsWith('label:')) labels.push(token.slice(6))
else if (token.startsWith('author:')) author = token.slice(7).replace(/^"|"$/g, '')
+ else if (token.startsWith('sort:')) {
+ const v = token.slice(5) as SortValue
+ if (VALID_SORTS.has(v)) sort = v
+ }
else free.push(token)
}
- return { status, labels, author, freeText: free.join(' ') }
+ return { status, labels, author, freeText: free.join(' '), sort }
}
function BugListSkeleton() {