feat : add TaskGitSection component
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
280
frontend/components/task/TaskGitSection.vue
Normal file
280
frontend/components/task/TaskGitSection.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="mt-5 rounded-lg border border-neutral-200 bg-neutral-50 p-4">
|
||||
<h3 class="text-sm font-bold text-neutral-900">{{ $t('gitea.branch.title') }}</h3>
|
||||
|
||||
<!-- Error state -->
|
||||
<p v-if="error" class="mt-2 text-sm text-red-500">{{ $t('gitea.error') }}</p>
|
||||
|
||||
<!-- Create branch form -->
|
||||
<div v-if="!showCreateForm" class="mt-3 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-primary-500 px-3 py-1.5 text-xs font-semibold text-white hover:bg-secondary-500"
|
||||
@click="showCreateForm = true"
|
||||
>
|
||||
{{ $t('gitea.branch.create') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-neutral-300 px-3 py-1.5 text-xs font-semibold text-neutral-600 hover:bg-neutral-100"
|
||||
@click="handleCopy"
|
||||
>
|
||||
{{ $t('gitea.branch.copy') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showCreateForm" class="mt-3 space-y-3 rounded-md border border-neutral-200 bg-white p-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<MalioSelect
|
||||
v-model="branchForm.type"
|
||||
:options="typeOptions"
|
||||
:label="$t('gitea.branch.type')"
|
||||
min-width="w-full"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="branchForm.baseBranch"
|
||||
:label="$t('gitea.branch.baseBranch')"
|
||||
input-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-neutral-500">{{ $t('gitea.branch.preview') }}</p>
|
||||
<code class="mt-1 block rounded bg-neutral-100 px-2 py-1 text-xs text-neutral-800">
|
||||
{{ branchPreview }}
|
||||
</code>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-primary-500 px-3 py-1.5 text-xs font-semibold text-white hover:bg-secondary-500 disabled:opacity-50"
|
||||
:disabled="isCreating"
|
||||
@click="handleCreate"
|
||||
>
|
||||
{{ $t('gitea.branch.create') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-neutral-300 px-3 py-1.5 text-xs font-semibold text-neutral-600 hover:bg-neutral-100"
|
||||
@click="showCreateForm = false"
|
||||
>
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="isLoading" class="mt-3 text-xs text-neutral-400">{{ $t('common.loading') }}</div>
|
||||
|
||||
<!-- Branches list -->
|
||||
<div v-if="!isLoading && branches.length" class="mt-4 space-y-3">
|
||||
<div v-for="branch in branches" :key="branch.name" class="rounded-md border border-neutral-200 bg-white p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="mdi:source-branch" size="16" class="text-primary-500" />
|
||||
<a
|
||||
:href="branchUrl(branch.name)"
|
||||
target="_blank"
|
||||
class="text-sm font-medium text-primary-500 hover:underline"
|
||||
>
|
||||
{{ branch.name }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Commits -->
|
||||
<div v-if="branch.commits.length" class="ml-6 mt-2 space-y-1">
|
||||
<p class="text-xs font-semibold text-neutral-500">{{ $t('gitea.branch.commits') }}</p>
|
||||
<div
|
||||
v-for="commit in branch.commits.slice(0, 5)"
|
||||
:key="commit.sha"
|
||||
class="flex items-baseline gap-2 text-xs"
|
||||
>
|
||||
<code class="text-primary-500">{{ commit.sha }}</code>
|
||||
<span class="truncate text-neutral-700">{{ commitFirstLine(commit.message) }}</span>
|
||||
<span class="whitespace-nowrap text-neutral-400">{{ commit.author }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="!isLoading && !branches.length && !error" class="mt-3 text-xs text-neutral-400">
|
||||
{{ $t('gitea.branch.noBranches') }}
|
||||
</p>
|
||||
|
||||
<!-- Pull Requests -->
|
||||
<div v-if="!isLoadingPrs && pullRequests.length" class="mt-4">
|
||||
<h4 class="text-xs font-bold text-neutral-700">{{ $t('gitea.pr.title') }}</h4>
|
||||
<div class="mt-2 space-y-2">
|
||||
<div v-for="pr in pullRequests" :key="pr.number" class="rounded-md border border-neutral-200 bg-white p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="rounded-full px-2 py-0.5 text-xs font-semibold text-white"
|
||||
:class="prStatusClass(pr)"
|
||||
>
|
||||
{{ prStatusLabel(pr) }}
|
||||
</span>
|
||||
<a
|
||||
:href="pr.url"
|
||||
target="_blank"
|
||||
class="text-sm font-medium text-primary-500 hover:underline"
|
||||
>
|
||||
#{{ pr.number }} {{ pr.title }}
|
||||
</a>
|
||||
<span class="text-xs text-neutral-400">{{ pr.author }}</span>
|
||||
</div>
|
||||
|
||||
<!-- CI statuses -->
|
||||
<div v-if="pr.ciStatuses.length" class="ml-6 mt-2 flex flex-wrap gap-2">
|
||||
<a
|
||||
v-for="ci in pr.ciStatuses"
|
||||
:key="ci.context"
|
||||
:href="ci.target_url"
|
||||
target="_blank"
|
||||
class="flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
:class="ciStatusClass(ci.status)"
|
||||
>
|
||||
<Icon :name="ciStatusIcon(ci.status)" size="12" />
|
||||
{{ ci.context }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="!isLoadingPrs && !pullRequests.length && branches.length && !error" class="mt-3 text-xs text-neutral-400">
|
||||
{{ $t('gitea.pr.noPrs') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Task } from '~/services/dto/task'
|
||||
import type { GiteaBranch, GiteaPullRequest } from '~/services/dto/gitea'
|
||||
import { useGiteaService } from '~/services/gitea'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps<{
|
||||
task: Task
|
||||
giteaUrl: string
|
||||
}>()
|
||||
|
||||
const { listBranches, createBranch, listPullRequests, getBranchName } = useGiteaService()
|
||||
|
||||
const branches = ref<GiteaBranch[]>([])
|
||||
const pullRequests = ref<GiteaPullRequest[]>([])
|
||||
const isLoading = ref(true)
|
||||
const isLoadingPrs = ref(true)
|
||||
const isCreating = ref(false)
|
||||
const error = ref(false)
|
||||
const showCreateForm = ref(false)
|
||||
|
||||
const branchForm = reactive({
|
||||
type: 'feature',
|
||||
baseBranch: 'develop',
|
||||
})
|
||||
|
||||
const typeOptions = [
|
||||
{ label: t('gitea.branch.types.feature'), value: 'feature' },
|
||||
{ label: t('gitea.branch.types.fix'), value: 'fix' },
|
||||
{ label: t('gitea.branch.types.refactor'), value: 'refactor' },
|
||||
{ label: t('gitea.branch.types.hotfix'), value: 'hotfix' },
|
||||
{ label: t('gitea.branch.types.chore'), value: 'chore' },
|
||||
]
|
||||
|
||||
const branchPreview = computed(() => {
|
||||
if (!props.task.project?.code || !props.task.number) return ''
|
||||
const slug = props.task.title
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 50)
|
||||
return `${branchForm.type}/${props.task.project.code}-${props.task.number}-${slug}`
|
||||
})
|
||||
|
||||
function branchUrl(name: string): string {
|
||||
const project = props.task.project
|
||||
if (!project?.giteaOwner || !project?.giteaRepo) return '#'
|
||||
return `${props.giteaUrl}/${project.giteaOwner}/${project.giteaRepo}/src/branch/${encodeURIComponent(name)}`
|
||||
}
|
||||
|
||||
function commitFirstLine(message: string): string {
|
||||
return message.split('\n')[0]
|
||||
}
|
||||
|
||||
function prStatusClass(pr: GiteaPullRequest): string {
|
||||
if (pr.merged) return 'bg-purple-500'
|
||||
if (pr.state === 'open') return 'bg-green-500'
|
||||
return 'bg-red-500'
|
||||
}
|
||||
|
||||
function prStatusLabel(pr: GiteaPullRequest): string {
|
||||
if (pr.merged) return t('gitea.pr.merged')
|
||||
if (pr.state === 'open') return t('gitea.pr.open')
|
||||
return t('gitea.pr.closed')
|
||||
}
|
||||
|
||||
function ciStatusClass(status: string): string {
|
||||
if (status === 'success') return 'bg-green-100 text-green-700'
|
||||
if (status === 'failure' || status === 'error') return 'bg-red-100 text-red-700'
|
||||
return 'bg-yellow-100 text-yellow-700'
|
||||
}
|
||||
|
||||
function ciStatusIcon(status: string): string {
|
||||
if (status === 'success') return 'mdi:check-circle'
|
||||
if (status === 'failure' || status === 'error') return 'mdi:close-circle'
|
||||
return 'mdi:clock-outline'
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (!props.task.id) return
|
||||
|
||||
isLoading.value = true
|
||||
isLoadingPrs.value = true
|
||||
error.value = false
|
||||
|
||||
try {
|
||||
branches.value = await listBranches(props.task.id)
|
||||
} catch {
|
||||
error.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
try {
|
||||
pullRequests.value = await listPullRequests(props.task.id)
|
||||
} catch {
|
||||
// PR errors don't block branch display
|
||||
} finally {
|
||||
isLoadingPrs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
isCreating.value = true
|
||||
try {
|
||||
await createBranch(props.task.id, {
|
||||
type: branchForm.type,
|
||||
baseBranch: branchForm.baseBranch,
|
||||
})
|
||||
showCreateForm.value = false
|
||||
await loadData()
|
||||
} finally {
|
||||
isCreating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopy() {
|
||||
try {
|
||||
const result = await getBranchName(props.task.id, branchForm.type)
|
||||
await navigator.clipboard.writeText(result.name)
|
||||
const { success } = useToast()
|
||||
success(t('gitea.branch.copied'))
|
||||
} catch {
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user