refactor(devops): SegmentedControl, design tokens, ConfirmDialog, spacing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e2f9f35362
commit
ba99a4dd65
2 changed files with 74 additions and 64 deletions
|
|
@ -3,6 +3,7 @@ import { ref } from 'vue'
|
|||
import { useDevopsStore } from '@/stores/devops'
|
||||
import Input from '@/components/ui/Input.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const devopsStore = useDevopsStore()
|
||||
|
|
@ -11,6 +12,7 @@ const adoOrg = ref(devopsStore.integration?.organization ?? '')
|
|||
const adoProject = ref(devopsStore.integration?.project ?? '')
|
||||
const adoPat = ref('')
|
||||
const saving = ref(false)
|
||||
const showDisconnectConfirm = ref(false)
|
||||
|
||||
async function save() {
|
||||
if (!adoOrg.value || !adoProject.value || !adoPat.value) {
|
||||
|
|
@ -34,7 +36,6 @@ async function save() {
|
|||
}
|
||||
|
||||
async function remove() {
|
||||
if (!confirm('Delete ADO integration?')) return
|
||||
try {
|
||||
await devopsStore.deleteIntegration()
|
||||
adoOrg.value = ''
|
||||
|
|
@ -88,10 +89,20 @@ async function remove() {
|
|||
v-if="devopsStore.integration"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
@click="remove"
|
||||
@click="showDisconnectConfirm = true"
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="showDisconnectConfirm"
|
||||
title="Disconnect Azure DevOps"
|
||||
description="This will remove the ADO integration and all synced work items. This action cannot be undone."
|
||||
confirm-label="Disconnect"
|
||||
cancel-label="Cancel"
|
||||
variant="destructive"
|
||||
@confirm="remove"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import Button from '@/components/ui/Button.vue'
|
|||
import Spinner from '@/components/ui/Spinner.vue'
|
||||
import DataTable from '@/components/ui/DataTable.vue'
|
||||
import type { TableColumn } from '@/components/ui/DataTable.vue'
|
||||
import SegmentedControl from '@/components/ui/SegmentedControl.vue'
|
||||
import Tooltip from '@/components/ui/Tooltip.vue'
|
||||
import DevopsConnectForm from '@/components/devops/DevopsConnectForm.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
|
|
@ -19,6 +21,13 @@ type StateFilter = 'All' | 'Active' | 'Resolved' | 'Closed'
|
|||
const stateFilter = ref<StateFilter>('All')
|
||||
const cloningId = ref<string | null>(null)
|
||||
|
||||
const stateOptions = [
|
||||
{ value: 'All', label: 'All' },
|
||||
{ value: 'Active', label: 'Active' },
|
||||
{ value: 'Resolved', label: 'Resolved' },
|
||||
{ value: 'Closed', label: 'Closed' },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
await devopsStore.fetchIntegration()
|
||||
if (devopsStore.integration) {
|
||||
|
|
@ -71,19 +80,18 @@ function priorityClass(p: number | null | undefined) {
|
|||
const v = p ?? 3
|
||||
if (v <= 1) return 'text-red-500 font-bold'
|
||||
if (v <= 2) return 'text-amber-500 font-semibold'
|
||||
return 'text-slate-400'
|
||||
return 'text-muted-foreground'
|
||||
}
|
||||
|
||||
function stateClass(s: string) {
|
||||
if (s === 'Active' || s === 'Doing' || s === 'In Progress') return 'bg-blue-50 text-blue-600 border border-blue-100'
|
||||
if (s === 'Resolved' || s === 'Done' || s === 'Closed') return 'bg-emerald-50 text-emerald-600 border border-emerald-100'
|
||||
if (s === 'New') return 'bg-slate-50 text-slate-500 border border-slate-200'
|
||||
return 'bg-slate-50 text-slate-500 border border-slate-200'
|
||||
return 'bg-muted text-muted-foreground border border-border'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-5">
|
||||
<div class="p-6 space-y-8">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap">
|
||||
<h2 class="text-lg font-semibold text-foreground">Azure DevOps</h2>
|
||||
<Button
|
||||
|
|
@ -105,17 +113,17 @@ function stateClass(s: string) {
|
|||
</template>
|
||||
<template v-else-if="devopsStore.integration">
|
||||
<div class="h-2 w-2 rounded-full bg-emerald-400 shadow-sm shadow-emerald-200" />
|
||||
<span class="text-sm text-slate-700">
|
||||
Connected to <strong class="text-slate-800">{{ devopsStore.integration.organization }}</strong>
|
||||
<span class="text-slate-400 mx-1">·</span>
|
||||
<span class="text-slate-500 text-xs">all assigned work items</span>
|
||||
<span class="text-sm text-foreground">
|
||||
Connected to <strong class="text-foreground">{{ devopsStore.integration.organization }}</strong>
|
||||
<span class="text-muted-foreground mx-1">·</span>
|
||||
<span class="text-muted-foreground text-xs">all assigned work items</span>
|
||||
</span>
|
||||
<span v-if="devopsStore.integration.last_synced_at" class="text-xs text-slate-400 ml-auto">
|
||||
<span v-if="devopsStore.integration.last_synced_at" class="text-xs text-muted-foreground ml-auto">
|
||||
Last synced: {{ new Date(devopsStore.integration.last_synced_at).toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="h-2 w-2 rounded-full bg-slate-300" />
|
||||
<div class="h-2 w-2 rounded-full bg-muted-foreground/30" />
|
||||
<span class="text-sm text-muted-foreground">Not connected</span>
|
||||
</template>
|
||||
<p v-if="devopsStore.integration?.last_sync_error" class="w-full text-xs text-destructive">
|
||||
|
|
@ -128,37 +136,26 @@ function stateClass(s: string) {
|
|||
<CardHeader>
|
||||
<CardTitle class="text-sm">Connect Azure DevOps</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent class="space-y-4">
|
||||
<DevopsConnectForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Work items table -->
|
||||
<div v-if="devopsStore.integration" class="space-y-3">
|
||||
<div v-if="devopsStore.integration" class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<h3 class="text-sm font-semibold text-slate-700">Work Items</h3>
|
||||
<!-- State filter tabs -->
|
||||
<div class="flex items-center rounded-xl border border-slate-200 overflow-hidden bg-white shadow-sm">
|
||||
<button
|
||||
v-for="state in (['All', 'Active', 'Resolved', 'Closed'] as const)"
|
||||
:key="state"
|
||||
:class="[
|
||||
'px-3 py-1.5 text-xs font-medium transition-colors',
|
||||
stateFilter === state
|
||||
? 'bg-orange-500 text-white'
|
||||
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-50',
|
||||
]"
|
||||
@click="stateFilter = state"
|
||||
>
|
||||
{{ state }}
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="text-sm font-semibold text-foreground">Work Items</h3>
|
||||
<SegmentedControl
|
||||
v-model="stateFilter"
|
||||
:options="stateOptions"
|
||||
aria-label="Filter by state"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="devopsStore.loading" class="flex items-center justify-center py-12">
|
||||
<Spinner size="md" class="text-primary" />
|
||||
</div>
|
||||
<div v-else class="bg-white rounded-xl shadow-sm border border-slate-200/80 overflow-hidden">
|
||||
<div v-else class="bg-background rounded-xl shadow-sm border border-border overflow-hidden">
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:rows="filteredWorkItems"
|
||||
|
|
@ -166,20 +163,20 @@ function stateClass(s: string) {
|
|||
>
|
||||
<!-- # column -->
|
||||
<template #cell-ado_id="{ value }">
|
||||
<span class="text-xs font-mono text-slate-400">#{{ value }}</span>
|
||||
<span class="text-xs font-mono text-muted-foreground">#{{ value }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Title column -->
|
||||
<template #cell-title="{ row }">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-slate-800 truncate font-medium">{{ row.title }}</p>
|
||||
<p class="text-xs text-slate-400 truncate">{{ row.type }}</p>
|
||||
<p class="text-sm text-foreground truncate font-medium">{{ row.title }}</p>
|
||||
<p class="text-xs text-muted-foreground truncate">{{ row.type }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Project column -->
|
||||
<template #cell-team_project="{ value }">
|
||||
<span class="text-xs text-slate-500 truncate block" :title="String(value ?? '')">
|
||||
<span class="text-xs text-muted-foreground truncate block" :title="String(value ?? '')">
|
||||
{{ value || '—' }}
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -196,7 +193,7 @@ function stateClass(s: string) {
|
|||
|
||||
<!-- Created column -->
|
||||
<template #cell-created_date="{ value }">
|
||||
<span class="text-xs text-slate-400">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ value ? new Date(String(value)).toLocaleDateString() : '—' }}
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -214,33 +211,35 @@ function stateClass(s: string) {
|
|||
<!-- Actions column -->
|
||||
<template #cell-id="{ row }">
|
||||
<div class="flex items-center gap-1 justify-end">
|
||||
<button
|
||||
class="p-1.5 rounded-lg text-slate-400 hover:text-orange-500 hover:bg-orange-50 transition-colors"
|
||||
:class="{ 'opacity-50': cloningId === String(row.id) }"
|
||||
title="Clone to Tasks"
|
||||
@click="cloneToTasks(String(row.id), $event)"
|
||||
>
|
||||
<svg v-if="cloningId !== String(row.id)" class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<svg v-else class="h-3.5 w-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<a
|
||||
v-if="row.url"
|
||||
:href="String(row.url)"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="p-1.5 rounded-lg text-slate-400 hover:text-blue-500 hover:bg-blue-50 transition-colors"
|
||||
title="Open in Azure DevOps"
|
||||
@click.stop
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
<Tooltip content="Clone to Tasks">
|
||||
<button
|
||||
class="p-1.5 rounded-lg text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
:class="{ 'opacity-50': cloningId === String(row.id) }"
|
||||
@click="cloneToTasks(String(row.id), $event)"
|
||||
>
|
||||
<svg v-if="cloningId !== String(row.id)" class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<svg v-else class="h-3.5 w-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Open in Azure DevOps">
|
||||
<a
|
||||
v-if="row.url"
|
||||
:href="String(row.url)"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="p-1.5 rounded-lg text-muted-foreground hover:text-blue-500 hover:bg-blue-50 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
@click.stop
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue