281 lines
10 KiB
Vue
281 lines
10 KiB
Vue
<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>
|