Skip to content
Merged
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
148 changes: 148 additions & 0 deletions app/components/issue/IssueRepoSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<script setup lang="ts">
const { t } = useI18n()
const repoStore = useRepositoryStore()
const issueStore = useIssueStore()
const { settings, update } = useUserSettings()

const open = ref(false)

const reposWithCounts = computed(() =>
repoStore.repos
.filter(r => !r.archived)
.map(r => ({
...r,
openIssues: repoStore.issueCounts[r.fullName] ?? 0,
openPrs: repoStore.prCounts[r.fullName] ?? 0,
}))
.sort((a, b) => b.openIssues - a.openIssues),
)

const selectedRepoData = computed(() =>
reposWithCounts.value.find(r => r.fullName === issueStore.selectedRepo),
)

async function select(fullName: string) {
open.value = false
await issueStore.selectRepo(fullName)
update({ selectedRepo: fullName })
}

// Restore from settings on mount
onMounted(async () => {
await repoStore.fetchAll()
const saved = settings.value?.selectedRepo
if (saved && repoStore.repos.some(r => r.fullName === saved)) {
await issueStore.selectRepo(saved)
}
})
</script>

<template>
<UPopover
v-model:open="open"
:content="{ side: 'bottom', align: 'start' }"
>
<UButton
variant="outline"
color="neutral"
class="w-full justify-between"
trailing-icon="i-lucide-chevrons-up-down"
>
<template v-if="selectedRepoData">
<div class="truncate flex justify-center font-medium">
<span>{{ selectedRepoData.name }}</span>

<UBadge
v-if="selectedRepoData.openIssues"
color="error"
variant="subtle"
size="xs"
class="ml-2 gap-1"
>
<UIcon
name="i-lucide-circle-dot"
class="size-3"
/>
{{ selectedRepoData.openIssues }}
</UBadge>
</div>
</template>
<span
v-else
class="text-muted"
>{{ t('issues.selectRepo') }}</span>
</UButton>

<template #content>
<div class="w-96 overflow-y-auto max-h-96">
<button
v-for="repo in reposWithCounts"
:key="repo.id"
class="w-full text-left px-3 py-2.5 hover:bg-elevated transition-colors flex items-start gap-3 cursor-pointer"
:class="{ 'bg-elevated/50': repo.fullName === issueStore.selectedRepo }"
@click="select(repo.fullName)"
>
<UIcon
:name="repo.fullName === issueStore.selectedRepo ? 'i-lucide-check' : 'i-lucide-book-marked'"
class="size-4 mt-0.5 shrink-0"
:class="repo.fullName === issueStore.selectedRepo ? 'text-primary' : 'text-muted'"
/>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="text-sm font-medium truncate">{{ repo.name }}</span>
<UBadge
v-if="repo.visibility === 'private'"
color="neutral"
variant="subtle"
size="xs"
>
{{ t('repos.badge.private') }}
</UBadge>
</div>
<p
v-if="repo.description"
class="text-xs text-muted truncate mt-0.5"
>
{{ repo.description }}
</p>
<div class="flex items-center gap-3 mt-1">
<span
v-if="repo.openIssues"
class="inline-flex items-center gap-1 text-xs text-rose-500"
>
<UIcon
name="i-lucide-circle-dot"
class="size-3.5"
/>
{{ t('issues.openCount', { count: repo.openIssues }) }}
</span>
<span
v-if="repo.openPrs"
class="inline-flex items-center gap-1 text-xs text-blue-500"
>
<UIcon
name="i-lucide-git-pull-request"
class="size-3.5"
/>
{{ repo.openPrs }}
</span>
<span
v-if="repo.language"
class="text-xs text-dimmed"
>
{{ repo.language }}
</span>
</div>
</div>
</button>

<p
v-if="!reposWithCounts.length"
class="px-3 py-4 text-sm text-muted text-center"
>
{{ t('repos.noResults') }}
</p>
</div>
</template>
</UPopover>
</template>
140 changes: 140 additions & 0 deletions app/components/issue/IssueRow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<script setup lang="ts">
import type { Issue } from '~~/shared/types/issue'

const props = defineProps<{
issue: Issue
}>()

const { t } = useI18n()
const createdAgo = useTimeAgo(computed(() => props.issue.createdAt))
const updatedAgo = useTimeAgo(computed(() => props.issue.updatedAt))

const stateIcon = computed(() => {
if (props.issue.state === 'OPEN') return 'i-lucide-circle-dot'
if (props.issue.stateReason === 'NOT_PLANNED') return 'i-lucide-circle-slash'
return 'i-lucide-check-circle'
})

const stateColor = computed(() => {
if (props.issue.state === 'OPEN') return 'text-emerald-500'
if (props.issue.stateReason === 'NOT_PLANNED') return 'text-neutral-400'
return 'text-violet-500'
})
</script>

<template>
<a
:href="issue.url"
target="_blank"
rel="noopener noreferrer"
class="flex items-start gap-3 px-4 py-3 hover:bg-elevated transition-colors"
>
<!-- State icon -->
<UIcon
:name="stateIcon"
class="size-5 mt-0.5 shrink-0"
:class="stateColor"
/>

<!-- Content -->
<div class="min-w-0 flex-1">
<!-- Row 1: Title + labels -->
<div class="flex items-center gap-2 flex-wrap">
<span class="font-medium text-highlighted hover:underline">
{{ issue.title }}
</span>
<UBadge
v-for="label in issue.labels"
:key="label.name"
variant="subtle"
size="xs"
:style="{ backgroundColor: `#${label.color}20`, color: `#${label.color}` }"
>
{{ label.name }}
</UBadge>
</div>

<!-- Row 2: Meta -->
<div class="flex items-center gap-3 mt-1 text-xs text-muted">
<UTooltip
v-if="!issue.maintainerCommented && issue.commentCount > 0"
:text="t('issues.needsResponse')"
>
<span class="inline-flex items-center gap-0.5 text-amber-500">
<UIcon
name="i-lucide-message-circle-warning"
class="size-3.5"
/>
</span>
</UTooltip>
<span class="inline-flex items-center gap-1">
<UAvatar
:src="issue.author.avatarUrl"
:alt="issue.author.login"
size="3xs"
/>
{{ issue.author.login }}
</span>
<span>#{{ issue.number }}</span>
<span>{{ createdAgo }}</span>
<span>{{ updatedAgo }}</span>
Comment on lines +57 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Repository name still missing from meta row.

PR objectives require displaying "title + number + repository name" to disambiguate issues across repositories. The author and created time from the prior feedback have been added, but the repository identifier is still absent.

🔧 Proposed fix to add repository name
       <!-- Row 2: Meta -->
       <div class="flex items-center gap-3 mt-1 text-xs text-muted">
+        <span class="inline-flex items-center gap-1">
+          <UIcon name="i-lucide-book-marked" class="size-3.5" />
+          {{ issue.repository }}
+        </span>
         <UTooltip
           v-if="!issue.maintainerCommented && issue.commentCount > 0"
           :text="t('issues.needsResponse')"
🤖 Prompt for AI Agents
In `@app/components/issue/IssueRow.vue` around lines 57 - 80, The meta row is
missing the repository identifier; update IssueRow.vue's template (near the span
showing {{ issue.number }}) to render the repository name (e.g., add a span like
{{ issue.repository.name }} or {{ issue.repoFullName }} depending on the issue
object shape) so the UI shows "title + number + repository name"; if the issue
prop doesn't currently include repository info, ensure the parent supplies
issue.repository (or repoFullName) from the API/prop source used by IssueRow.vue
(keep placement alongside {{ issue.number }} and maintain styling consistent
with createdAgo/updatedAgo).


<span
v-if="issue.commentCount"
class="inline-flex items-center gap-0.5"
>
<UIcon
name="i-lucide-message-square"
class="size-3.5"
/>
{{ issue.commentCount }}
</span>

<span
v-if="issue.linkedPrCount"
class="inline-flex items-center gap-0.5 text-blue-500"
>
<UIcon
name="i-lucide-git-pull-request"
class="size-3.5"
/>
{{ issue.linkedPrCount }}
</span>

<span
v-if="issue.state === 'CLOSED'"
class="inline-flex items-center gap-0.5"
:class="issue.stateReason === 'NOT_PLANNED' ? 'text-neutral-400' : 'text-violet-500'"
>
{{ issue.stateReason === 'NOT_PLANNED' ? t('issues.closedAsNotPlanned') : t('issues.closedAs') }}
</span>

<span
v-if="issue.milestone"
class="inline-flex items-center gap-0.5"
>
<UIcon
name="i-lucide-milestone"
class="size-3.5"
/>
{{ issue.milestone }}
</span>
</div>
</div>

<!-- Right side: Assignees + author -->
<div class="flex items-center gap-1 shrink-0">
<UTooltip
v-for="assignee in issue.assignees"
:key="assignee.login"
:text="assignee.login"
>
<UAvatar
:src="assignee.avatarUrl"
:alt="assignee.login"
size="xs"
/>
</UTooltip>
</div>
</a>
Comment on lines +8 to +139
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing required row metadata (repo, author, created time).

PR objectives call for repository name, author avatar/name, and relative created time in each row. Currently only updated time and assignees are rendered, so the overview can’t disambiguate cross‑repo issues or show author/created info.

💡 Minimal sketch of the missing metadata wiring
<script setup lang="ts">
@@
 const { t } = useI18n()
 const updatedAgo = useTimeAgo(computed(() => props.issue.updatedAt))
+const createdAgo = useTimeAgo(computed(() => props.issue.createdAt))
@@
</script>
<!-- Row 2: Meta -->
 <div class="flex items-center gap-3 mt-1 text-xs text-muted">
+  <span class="inline-flex items-center gap-1">
+    <UIcon name="i-lucide-book" class="size-3.5" />
+    {{ issue.repository.nameWithOwner }}
+  </span>
+  <span class="inline-flex items-center gap-1">
+    <UAvatar :src="issue.author.avatarUrl" :alt="issue.author.login" size="xs" />
+    {{ issue.author.login }}
+  </span>
   <span>#{{ issue.number }}</span>
+  <span>{{ createdAgo }}</span>
   <span>{{ updatedAgo }}</span>
   ...
 </div>
🤖 Prompt for AI Agents
In `@app/components/issue/IssueRow.vue` around lines 8 - 129, The row is missing
repository name, author (avatar + name) and relative created time; add a new
computed createdAgo (e.g. useTimeAgo(computed(() => props.issue.createdAt))) and
render the repo name, author avatar/name and createdAgo in the Row 2 meta next
to the existing number/updated items. Specifically, update IssueRow.vue to: 1)
add createdAgo computed like updatedAgo, 2) render a repository label (e.g.
issue.repository or issue.repoName) before the issue number to disambiguate
cross‑repo rows, and 3) render the author use (issue.author.login and
issue.author.avatarUrl) as a small UAvatar + name and show createdAgo (relative
created time) in the meta section; use the existing UAvatar, UTooltip and UIcon
components and the props.issue fields (createdAt, repository/repoName,
author.login, author.avatarUrl) so the new elements integrate with the current
template layout.

</template>
92 changes: 92 additions & 0 deletions app/components/issue/IssueToolbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<script setup lang="ts">
const { t } = useI18n()
const store = useIssueStore()

function toggleFilter(key: string) {
const current = store.activeFilters
if (current.includes(key)) {
store.activeFilters = current.filter(f => f !== key)
}
else {
store.activeFilters = [...current, key]
}
}

const filterChips = computed(() => [
{ key: 'unassigned', label: t('issues.filter.unassigned'), icon: 'i-lucide-user-x' },
{ key: 'hasLinkedPr', label: t('issues.filter.hasLinkedPr'), icon: 'i-lucide-git-pull-request', color: 'text-blue-500' },
{ key: 'noLinkedPr', label: t('issues.filter.noLinkedPr'), icon: 'i-lucide-git-pull-request-closed', color: 'text-rose-500' },
{ key: 'hasMilestone', label: t('issues.filter.hasMilestone'), icon: 'i-lucide-milestone' },
])

const sortOptions = computed(() => [
{ label: t('issues.sort.critical'), value: 'critical' },
{ label: t('issues.sort.newest'), value: 'newest' },
{ label: t('issues.sort.oldest'), value: 'oldest' },
{ label: t('issues.sort.mostCommented'), value: 'mostCommented' },
{ label: t('issues.sort.leastCommented'), value: 'leastCommented' },
{ label: t('issues.sort.recentlyUpdated'), value: 'recentlyUpdated' },
])
</script>

<template>
<div class="flex flex-col gap-3">
<!-- Row 1: Search + count -->
<div class="flex items-center gap-3">
<UInput
v-model="store.search"
:placeholder="t('issues.search')"
icon="i-lucide-search"
class="flex-1"
/>
<span class="text-sm text-muted shrink-0">
{{ t('issues.count', store.filteredIssues.length) }}
</span>
</div>

<!-- Row 2: Filter chips + sort -->
<div class="flex items-center gap-2 flex-wrap">
<button
v-for="chip in filterChips"
:key="chip.key"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium transition-colors cursor-pointer"
:class="store.activeFilters.includes(chip.key)
? (chip.color ? `bg-muted ${chip.color} ring-1 ring-current/20` : 'bg-primary text-inverted')
: 'bg-muted text-toned hover:bg-accented'"
@click="toggleFilter(chip.key)"
>
<UIcon
:name="chip.icon"
class="size-3.5"
/>
{{ chip.label }}
</button>

<div class="ml-auto">
<USelect
v-model="store.sortKey"
:items="sortOptions"
size="xs"
/>
</div>
</div>

<!-- Row 3: Label chips -->
<div
v-if="store.availableLabels.length"
class="flex items-center gap-1.5 flex-wrap"
>
<button
v-for="label in store.availableLabels"
:key="label"
class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs transition-colors cursor-pointer"
:class="store.activeFilters.includes(`label:${label}`)
? 'bg-primary text-inverted'
: 'bg-muted text-toned hover:bg-accented'"
@click="toggleFilter(`label:${label}`)"
>
{{ label }}
</button>
</div>
</div>
</template>
Loading
Loading