1153 lines
52 KiB
Vue
1153 lines
52 KiB
Vue
<script setup lang="ts">
|
|
import { Head, Link, router, useForm } from '@inertiajs/vue3';
|
|
import {
|
|
ExternalLink,
|
|
GitBranch,
|
|
Link2,
|
|
Pencil,
|
|
Plus,
|
|
RefreshCw,
|
|
Search,
|
|
Server,
|
|
Trash2,
|
|
} from 'lucide-vue-next';
|
|
import { computed, ref, watch } from 'vue';
|
|
import {
|
|
destroy,
|
|
store,
|
|
update,
|
|
} from '@/actions/App/Http/Controllers/ProjectRepositorySettingsController';
|
|
import FeedbackState from '@/components/FeedbackState.vue';
|
|
import FormFeedback from '@/components/FormFeedback.vue';
|
|
import InputError from '@/components/InputError.vue';
|
|
import TemplateInheritancePanel from '@/components/projects/TemplateInheritancePanel.vue';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { useFormProtection } from '@/composables/useFormProtection';
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import AppLayout from '@/layouts/AppLayout.vue';
|
|
import SettingsLayout from '@/layouts/projects/SettingsLayout.vue';
|
|
import { edit as editGitConnections } from '@/routes/git-connections/index';
|
|
import { index, show } from '@/routes/projects/index';
|
|
import { repositories } from '@/routes/projects/settings';
|
|
import type {
|
|
BreadcrumbItem,
|
|
ProjectRepositorySettingsPageProps,
|
|
} from '@/types';
|
|
import type { AvailableGitRepositorySourceModel } from '@/types/models/available-git-repository-source-model';
|
|
import type { RepositoryConnectionModel } from '@/types/models/repository-connection-model';
|
|
|
|
type DialogMode = 'link' | 'edit' | 'remove';
|
|
|
|
const props = defineProps<ProjectRepositorySettingsPageProps>();
|
|
const { t } = useLocalization();
|
|
const breadcrumbs = computed<BreadcrumbItem[]>(() => [
|
|
{ title: t('navigation.projects'), href: index() },
|
|
{ title: props.project.name, href: show(props.project.id) },
|
|
{
|
|
title: t('navigation.repositories'),
|
|
href: repositories(props.project.id),
|
|
},
|
|
]);
|
|
const dialogOpen = ref(false);
|
|
const dialogMode = ref<DialogMode>('link');
|
|
const selectedConnection = ref<RepositoryConnectionModel | null>(null);
|
|
const repositorySearch = ref('');
|
|
const refreshingSources = ref(false);
|
|
const repositoryForm = useForm<{
|
|
provider_repository_id: string;
|
|
user_git_connection_id: string;
|
|
}>({
|
|
provider_repository_id: '',
|
|
user_git_connection_id: '',
|
|
});
|
|
const removalForm = useForm({});
|
|
const repositoryProtection = useFormProtection(repositoryForm);
|
|
const removalProtection = useFormProtection(removalForm);
|
|
const processing = computed(
|
|
() => repositoryForm.processing || removalForm.processing,
|
|
);
|
|
const successfulSourceCount = computed(
|
|
() =>
|
|
props.availableRepositorySources.filter((source) => !source.error)
|
|
.length,
|
|
);
|
|
const selectedSource = computed(() =>
|
|
props.availableRepositorySources.find(
|
|
(source) =>
|
|
source.user_git_connection_id ===
|
|
Number(repositoryForm.user_git_connection_id),
|
|
),
|
|
);
|
|
const filteredRepositories = computed(() => {
|
|
const search = repositorySearch.value.trim().toLocaleLowerCase();
|
|
const sourceRepositories = selectedSource.value?.repositories ?? [];
|
|
|
|
if (search === '') {
|
|
return sourceRepositories;
|
|
}
|
|
|
|
return sourceRepositories.filter((repository) =>
|
|
[
|
|
repository.full_name,
|
|
repository.owner_name,
|
|
repository.repository_name,
|
|
]
|
|
.join(' ')
|
|
.toLocaleLowerCase()
|
|
.includes(search),
|
|
);
|
|
});
|
|
const canSaveRepository = computed(
|
|
() =>
|
|
selectedSource.value !== undefined &&
|
|
selectedSource.value.error === null &&
|
|
selectedSource.value.repositories.some(
|
|
(repository) =>
|
|
repository.provider_repository_id ===
|
|
repositoryForm.provider_repository_id,
|
|
),
|
|
);
|
|
const matchingPersonalSourceMissing = computed(
|
|
() =>
|
|
dialogMode.value === 'edit' &&
|
|
selectedConnection.value !== null &&
|
|
!props.availableRepositorySources.some(
|
|
(source) =>
|
|
source.provider === selectedConnection.value?.provider &&
|
|
source.base_url === selectedConnection.value?.base_url,
|
|
),
|
|
);
|
|
const dialogTitle = computed(() =>
|
|
t(
|
|
dialogMode.value === 'link'
|
|
? 'repositorySettings.linkTitle'
|
|
: dialogMode.value === 'edit'
|
|
? 'repositorySettings.editTitle'
|
|
: 'repositorySettings.removeTitle',
|
|
),
|
|
);
|
|
const successTitle = computed(() => {
|
|
if (props.status?.endsWith('connection was updated.')) {
|
|
return t('repositorySettings.updated');
|
|
}
|
|
|
|
if (props.status?.endsWith('was removed from the project.')) {
|
|
return t('repositorySettings.removed');
|
|
}
|
|
|
|
return t('repositorySettings.linked');
|
|
});
|
|
|
|
function sourceForConnection(
|
|
repositoryConnection: RepositoryConnectionModel | null,
|
|
): AvailableGitRepositorySourceModel | undefined {
|
|
if (repositoryConnection) {
|
|
const matchingSource = props.availableRepositorySources.find(
|
|
(source) =>
|
|
source.provider === repositoryConnection.provider &&
|
|
source.base_url === repositoryConnection.base_url,
|
|
);
|
|
|
|
if (matchingSource) {
|
|
return matchingSource;
|
|
}
|
|
}
|
|
|
|
return (
|
|
props.availableRepositorySources.find(
|
|
(source) => source.error === null && source.repositories.length > 0,
|
|
) ??
|
|
props.availableRepositorySources.find(
|
|
(source) => source.error === null,
|
|
) ??
|
|
props.availableRepositorySources[0]
|
|
);
|
|
}
|
|
|
|
function resetForms(): void {
|
|
repositoryForm.defaults({
|
|
provider_repository_id: '',
|
|
user_git_connection_id: '',
|
|
});
|
|
repositoryForm.resetAndClearErrors();
|
|
repositoryProtection.clearFeedback();
|
|
removalForm.resetAndClearErrors();
|
|
removalProtection.clearFeedback();
|
|
repositorySearch.value = '';
|
|
}
|
|
|
|
function openDialog(
|
|
mode: DialogMode,
|
|
repositoryConnection: RepositoryConnectionModel | null = null,
|
|
): void {
|
|
resetForms();
|
|
dialogMode.value = mode;
|
|
selectedConnection.value = repositoryConnection;
|
|
|
|
if (mode !== 'remove') {
|
|
const source = sourceForConnection(repositoryConnection);
|
|
const currentRepositoryId =
|
|
repositoryConnection?.provider_repository_id;
|
|
const providerRepositoryId = source?.repositories.some(
|
|
(repository) =>
|
|
repository.provider_repository_id === currentRepositoryId,
|
|
)
|
|
? (currentRepositoryId ?? '')
|
|
: '';
|
|
|
|
repositoryForm.defaults({
|
|
provider_repository_id: providerRepositoryId,
|
|
user_git_connection_id: String(
|
|
source?.user_git_connection_id ?? '',
|
|
),
|
|
});
|
|
repositoryForm.reset();
|
|
}
|
|
|
|
dialogOpen.value = true;
|
|
}
|
|
|
|
function closeDialog(open: boolean): void {
|
|
if (
|
|
open ||
|
|
processing.value ||
|
|
!repositoryProtection.confirmLeave() ||
|
|
!removalProtection.confirmLeave()
|
|
) {
|
|
return;
|
|
}
|
|
|
|
dialogOpen.value = false;
|
|
resetForms();
|
|
}
|
|
|
|
function finishMutation(): void {
|
|
dialogOpen.value = false;
|
|
selectedConnection.value = null;
|
|
resetForms();
|
|
}
|
|
|
|
function selectSource(source: AvailableGitRepositorySourceModel): void {
|
|
repositoryForm.user_git_connection_id = String(
|
|
source.user_git_connection_id,
|
|
);
|
|
}
|
|
|
|
function selectRepository(providerRepositoryId: string): void {
|
|
repositoryForm.provider_repository_id = providerRepositoryId;
|
|
repositoryForm.clearErrors('provider_repository_id');
|
|
}
|
|
|
|
function saveRepository(): void {
|
|
if (!canSaveRepository.value || processing.value) {
|
|
return;
|
|
}
|
|
|
|
const target =
|
|
dialogMode.value === 'edit' && selectedConnection.value
|
|
? update([props.project.id, selectedConnection.value.id])
|
|
: store(props.project.id);
|
|
|
|
repositoryProtection.submit(target, {
|
|
preserveScroll: true,
|
|
onSuccess: finishMutation,
|
|
});
|
|
}
|
|
|
|
function removeRepository(): void {
|
|
if (!selectedConnection.value || processing.value) {
|
|
return;
|
|
}
|
|
|
|
removalProtection.submit(
|
|
destroy([props.project.id, selectedConnection.value.id]),
|
|
{
|
|
preserveScroll: true,
|
|
onSuccess: finishMutation,
|
|
},
|
|
);
|
|
}
|
|
|
|
function refreshSources(): void {
|
|
if (refreshingSources.value || processing.value) {
|
|
return;
|
|
}
|
|
|
|
router.reload({
|
|
only: ['availableRepositorySources'],
|
|
onStart: () => {
|
|
refreshingSources.value = true;
|
|
},
|
|
onFinish: () => {
|
|
refreshingSources.value = false;
|
|
},
|
|
});
|
|
}
|
|
|
|
watch(
|
|
() => repositoryForm.user_git_connection_id,
|
|
() => {
|
|
repositorySearch.value = '';
|
|
|
|
if (
|
|
selectedSource.value?.repositories.some(
|
|
(repository) =>
|
|
repository.provider_repository_id ===
|
|
repositoryForm.provider_repository_id,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
repositoryForm.provider_repository_id = '';
|
|
repositoryForm.clearErrors('provider_repository_id');
|
|
},
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
|
<Head
|
|
:title="
|
|
t('repositorySettings.pageTitle', { project: project.name })
|
|
"
|
|
/>
|
|
<SettingsLayout :project-name="project.name" :project-key="project.key">
|
|
<FeedbackState
|
|
v-if="status"
|
|
state="success"
|
|
:title="successTitle"
|
|
/>
|
|
|
|
<TemplateInheritancePanel
|
|
:areas="['repository_connections']"
|
|
:project-id="project.id"
|
|
:template-state="templateState"
|
|
/>
|
|
|
|
<Card
|
|
class="min-w-0 gap-0 overflow-hidden rounded-xl py-0 shadow-none"
|
|
>
|
|
<CardHeader
|
|
class="gap-column min-w-0 grid-cols-1 border-b p-5 sm:p-6"
|
|
>
|
|
<div
|
|
class="flex min-w-0 flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
|
>
|
|
<div class="gap-stack grid min-w-0">
|
|
<h2 class="text-title">
|
|
{{ t('repositorySettings.title') }}
|
|
</h2>
|
|
<p class="text-muted-foreground">
|
|
{{ t('repositorySettings.description') }}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
class="shrink-0 self-start"
|
|
@click="openDialog('link')"
|
|
>
|
|
<Plus aria-hidden="true" />
|
|
{{ t('repositorySettings.linkAction') }}
|
|
</Button>
|
|
</div>
|
|
<div
|
|
class="bg-muted/45 text-caption text-muted-foreground flex items-start gap-2 rounded-lg border p-3"
|
|
>
|
|
<Link2
|
|
class="text-primary mt-0.5 size-4 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<p>
|
|
{{ t('repositorySettings.projectScope') }}
|
|
</p>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent class="gap-column grid min-w-0 p-5 sm:p-6">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<h3 class="font-semibold">
|
|
{{ t('repositorySettings.linkedRepositories') }}
|
|
</h3>
|
|
<Badge variant="secondary">
|
|
{{ repositoryConnections.length }}
|
|
</Badge>
|
|
</div>
|
|
|
|
<FeedbackState
|
|
v-if="repositoryConnections.length === 0"
|
|
state="empty"
|
|
:title="t('repositorySettings.empty')"
|
|
:description="t('repositorySettings.emptyDescription')"
|
|
>
|
|
<Button type="button" @click="openDialog('link')">
|
|
<Plus aria-hidden="true" />
|
|
{{ t('repositorySettings.linkAction') }}
|
|
</Button>
|
|
</FeedbackState>
|
|
|
|
<ul
|
|
v-else
|
|
class="gap-column grid min-w-0"
|
|
:aria-label="t('repositorySettings.linkedRepositories')"
|
|
>
|
|
<li
|
|
v-for="repositoryConnection in repositoryConnections"
|
|
:key="repositoryConnection.id"
|
|
class="gap-column grid min-w-0 rounded-lg border p-4"
|
|
>
|
|
<div class="flex min-w-0 items-start gap-3">
|
|
<div
|
|
class="bg-accent text-primary flex size-10 shrink-0 items-center justify-center rounded-lg"
|
|
>
|
|
<GitBranch
|
|
class="size-5"
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
<div class="gap-stack grid min-w-0 flex-1">
|
|
<div
|
|
class="flex min-w-0 flex-wrap items-center gap-2"
|
|
>
|
|
<h4
|
|
class="min-w-0 break-all font-semibold"
|
|
>
|
|
{{
|
|
repositoryConnection.repository_full_name
|
|
}}
|
|
</h4>
|
|
<Badge variant="secondary">
|
|
{{
|
|
repositoryConnection.provider_label
|
|
}}
|
|
</Badge>
|
|
<Badge
|
|
:variant="
|
|
repositoryConnection.has_personal_access
|
|
? 'success'
|
|
: 'warning'
|
|
"
|
|
>
|
|
{{
|
|
t(
|
|
repositoryConnection.has_personal_access
|
|
? 'repositorySettings.personalAccessReady'
|
|
: 'repositorySettings.personalAccessMissing',
|
|
)
|
|
}}
|
|
</Badge>
|
|
</div>
|
|
<div
|
|
class="text-caption text-muted-foreground flex min-w-0 items-start gap-2"
|
|
>
|
|
<Server
|
|
class="mt-0.5 size-4 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<span class="min-w-0 break-all">
|
|
{{ repositoryConnection.base_url }}
|
|
</span>
|
|
</div>
|
|
<p
|
|
class="text-caption text-muted-foreground"
|
|
>
|
|
{{
|
|
t('repositorySettings.ticketUsage')
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="flex flex-wrap gap-2 border-t pt-3 sm:justify-end"
|
|
>
|
|
<Button
|
|
v-if="
|
|
repositoryConnection.has_personal_access
|
|
"
|
|
as-child
|
|
variant="outline"
|
|
>
|
|
<a
|
|
:href="
|
|
repositoryConnection.repository_url
|
|
"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
:aria-label="
|
|
t(
|
|
'repositorySettings.openRepository',
|
|
{
|
|
repository:
|
|
repositoryConnection.repository_full_name,
|
|
},
|
|
)
|
|
"
|
|
>
|
|
<ExternalLink aria-hidden="true" />
|
|
{{ t('repositorySettings.open') }}
|
|
</a>
|
|
</Button>
|
|
<Button v-else as-child variant="outline">
|
|
<Link :href="editGitConnections()">
|
|
<Link2 aria-hidden="true" />
|
|
{{
|
|
t(
|
|
'repositorySettings.connectAccess',
|
|
)
|
|
}}
|
|
</Link>
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
:aria-label="
|
|
t('repositorySettings.editRepository', {
|
|
repository:
|
|
repositoryConnection.repository_full_name,
|
|
})
|
|
"
|
|
@click="
|
|
openDialog('edit', repositoryConnection)
|
|
"
|
|
>
|
|
<Pencil aria-hidden="true" />
|
|
{{ t('common.edit') }}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
:aria-label="
|
|
t(
|
|
'repositorySettings.removeRepository',
|
|
{
|
|
repository:
|
|
repositoryConnection.repository_full_name,
|
|
},
|
|
)
|
|
"
|
|
@click="
|
|
openDialog(
|
|
'remove',
|
|
repositoryConnection,
|
|
)
|
|
"
|
|
>
|
|
<Trash2 aria-hidden="true" />
|
|
{{ t('common.remove') }}
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card
|
|
class="min-w-0 gap-0 overflow-hidden rounded-xl py-0 shadow-none"
|
|
>
|
|
<CardHeader
|
|
class="gap-column min-w-0 grid-cols-1 border-b p-5 sm:p-6"
|
|
>
|
|
<div
|
|
class="flex min-w-0 flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
|
>
|
|
<div class="gap-stack grid min-w-0">
|
|
<h2 class="text-title">
|
|
{{ t('repositorySettings.personalSources') }}
|
|
</h2>
|
|
<p class="text-muted-foreground">
|
|
{{
|
|
t(
|
|
'repositorySettings.personalSourcesDescription',
|
|
)
|
|
}}
|
|
</p>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<Button as-child variant="outline">
|
|
<Link :href="editGitConnections()">
|
|
{{
|
|
t(
|
|
'repositorySettings.manageConnections',
|
|
)
|
|
}}
|
|
</Link>
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
:loading="refreshingSources"
|
|
:loading-label="
|
|
t('repositorySettings.refreshing')
|
|
"
|
|
@click="refreshSources"
|
|
>
|
|
<RefreshCw aria-hidden="true" />
|
|
{{ t('repositorySettings.refresh') }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent class="gap-column grid min-w-0 p-5 sm:p-6">
|
|
<FeedbackState
|
|
v-if="refreshingSources"
|
|
state="loading"
|
|
:title="t('repositorySettings.refreshing')"
|
|
:description="
|
|
t('repositorySettings.refreshingDescription')
|
|
"
|
|
/>
|
|
<FeedbackState
|
|
v-else-if="availableRepositorySources.length === 0"
|
|
state="empty"
|
|
:title="t('repositorySettings.noConnections')"
|
|
:description="
|
|
t('repositorySettings.noConnectionsDescription')
|
|
"
|
|
>
|
|
<Button as-child variant="outline">
|
|
<Link :href="editGitConnections()">
|
|
{{ t('repositorySettings.manageConnections') }}
|
|
</Link>
|
|
</Button>
|
|
</FeedbackState>
|
|
<template v-else>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<h3 class="font-semibold">
|
|
{{ t('repositorySettings.detectedSources') }}
|
|
</h3>
|
|
<Badge variant="secondary">
|
|
{{ successfulSourceCount }} /
|
|
{{ availableRepositorySources.length }}
|
|
</Badge>
|
|
</div>
|
|
<ul class="gap-column grid min-w-0">
|
|
<li
|
|
v-for="source in availableRepositorySources"
|
|
:key="source.user_git_connection_id"
|
|
class="gap-card grid min-w-0 rounded-lg border p-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start"
|
|
>
|
|
<div class="flex min-w-0 items-start gap-3">
|
|
<div
|
|
class="bg-muted text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded-md"
|
|
>
|
|
<Server
|
|
class="size-4"
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
<div class="gap-stack grid min-w-0">
|
|
<div
|
|
class="flex min-w-0 flex-wrap items-center gap-2"
|
|
>
|
|
<span
|
|
class="min-w-0 break-all font-semibold"
|
|
>
|
|
{{ source.account_login }}
|
|
</span>
|
|
<Badge variant="secondary">
|
|
{{ source.provider_label }}
|
|
</Badge>
|
|
</div>
|
|
<span
|
|
class="text-caption text-muted-foreground break-all"
|
|
>
|
|
{{ source.base_url }}
|
|
</span>
|
|
<p
|
|
v-if="source.error"
|
|
class="text-caption text-destructive break-words"
|
|
>
|
|
{{ source.error }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Badge
|
|
:variant="
|
|
source.error ? 'warning' : 'outline'
|
|
"
|
|
class="justify-self-start"
|
|
>
|
|
{{
|
|
source.error
|
|
? t(
|
|
'repositorySettings.unavailable',
|
|
)
|
|
: t(
|
|
'repositorySettings.repositoryCount',
|
|
{
|
|
count: source.repositories
|
|
.length,
|
|
},
|
|
)
|
|
}}
|
|
</Badge>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Dialog :open="dialogOpen" @update:open="closeDialog">
|
|
<DialogContent
|
|
class="sm:max-w-2xl"
|
|
:show-close-button="!processing"
|
|
@escape-key-down="processing && $event.preventDefault()"
|
|
@interact-outside="processing && $event.preventDefault()"
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle>{{ dialogTitle }}</DialogTitle>
|
|
<DialogDescription>
|
|
{{
|
|
t(
|
|
dialogMode === 'link'
|
|
? 'repositorySettings.linkDescription'
|
|
: dialogMode === 'edit'
|
|
? 'repositorySettings.editDescription'
|
|
: 'repositorySettings.removeDescription',
|
|
)
|
|
}}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form
|
|
v-if="dialogMode !== 'remove'"
|
|
class="gap-column grid min-w-0"
|
|
:aria-busy="repositoryForm.processing"
|
|
@submit.prevent="saveRepository"
|
|
>
|
|
<FormFeedback
|
|
:state="repositoryProtection.state.value"
|
|
:title="
|
|
repositoryProtection.responseStatus.value ===
|
|
403
|
|
? t('repositorySettings.forbidden')
|
|
: repositoryForm.processing
|
|
? t(
|
|
dialogMode === 'edit'
|
|
? 'repositorySettings.saving'
|
|
: 'repositorySettings.linking',
|
|
)
|
|
: undefined
|
|
"
|
|
:description="
|
|
repositoryProtection.responseStatus.value ===
|
|
403
|
|
? t(
|
|
'repositorySettings.forbiddenDescription',
|
|
)
|
|
: undefined
|
|
"
|
|
:retryable="
|
|
repositoryProtection.responseStatus.value !==
|
|
403
|
|
"
|
|
@retry="saveRepository"
|
|
/>
|
|
|
|
<FeedbackState
|
|
v-if="matchingPersonalSourceMissing"
|
|
state="empty"
|
|
:title="
|
|
t(
|
|
'repositorySettings.matchingConnectionMissing',
|
|
)
|
|
"
|
|
:description="
|
|
t(
|
|
'repositorySettings.matchingConnectionMissingDescription',
|
|
)
|
|
"
|
|
>
|
|
<Button as-child variant="outline">
|
|
<Link :href="editGitConnections()">
|
|
{{
|
|
t(
|
|
'repositorySettings.manageConnections',
|
|
)
|
|
}}
|
|
</Link>
|
|
</Button>
|
|
</FeedbackState>
|
|
|
|
<fieldset
|
|
class="gap-column grid min-w-0"
|
|
:disabled="repositoryForm.processing"
|
|
>
|
|
<legend class="sr-only">{{ dialogTitle }}</legend>
|
|
|
|
<div class="gap-stack grid min-w-0">
|
|
<div
|
|
class="flex flex-wrap items-baseline justify-between gap-2"
|
|
>
|
|
<Label>
|
|
{{
|
|
t(
|
|
'repositorySettings.personalConnection',
|
|
)
|
|
}}
|
|
</Label>
|
|
<span
|
|
class="text-caption text-muted-foreground"
|
|
>
|
|
{{
|
|
t(
|
|
'repositorySettings.personalConnectionHelp',
|
|
)
|
|
}}
|
|
</span>
|
|
</div>
|
|
|
|
<FeedbackState
|
|
v-if="
|
|
availableRepositorySources.length === 0
|
|
"
|
|
state="empty"
|
|
:title="
|
|
t('repositorySettings.noConnections')
|
|
"
|
|
:description="
|
|
t(
|
|
'repositorySettings.noConnectionsDescription',
|
|
)
|
|
"
|
|
>
|
|
<Button as-child variant="outline">
|
|
<Link :href="editGitConnections()">
|
|
{{
|
|
t(
|
|
'repositorySettings.manageConnections',
|
|
)
|
|
}}
|
|
</Link>
|
|
</Button>
|
|
</FeedbackState>
|
|
|
|
<div
|
|
v-else
|
|
class="grid min-w-0 gap-2 sm:grid-cols-2"
|
|
>
|
|
<button
|
|
v-for="source in availableRepositorySources"
|
|
:key="source.user_git_connection_id"
|
|
type="button"
|
|
:aria-pressed="
|
|
repositoryForm.user_git_connection_id ===
|
|
String(
|
|
source.user_git_connection_id,
|
|
)
|
|
"
|
|
:class="[
|
|
'focus-visible:ring-ring/35 gap-stack grid min-w-0 rounded-lg border p-3 text-left outline-none transition-colors focus-visible:ring-2',
|
|
repositoryForm.user_git_connection_id ===
|
|
String(
|
|
source.user_git_connection_id,
|
|
)
|
|
? 'border-primary/50 bg-accent'
|
|
: 'hover:bg-muted/50',
|
|
]"
|
|
@click="selectSource(source)"
|
|
>
|
|
<span
|
|
class="flex min-w-0 flex-wrap items-center gap-2"
|
|
>
|
|
<span
|
|
class="min-w-0 break-all font-semibold"
|
|
>
|
|
{{ source.account_login }}
|
|
</span>
|
|
<Badge variant="secondary">
|
|
{{ source.provider_label }}
|
|
</Badge>
|
|
</span>
|
|
<span
|
|
class="text-caption text-muted-foreground break-all"
|
|
>
|
|
{{ source.base_url }}
|
|
</span>
|
|
<span
|
|
v-if="source.error"
|
|
class="text-caption text-destructive"
|
|
>
|
|
{{
|
|
t(
|
|
'repositorySettings.unavailable',
|
|
)
|
|
}}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<InputError
|
|
:message="
|
|
repositoryForm.errors
|
|
.user_git_connection_id
|
|
"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="selectedSource" class="gap-column grid">
|
|
<FeedbackState
|
|
v-if="selectedSource.error"
|
|
state="error"
|
|
:title="
|
|
t('repositorySettings.providerError')
|
|
"
|
|
:description="selectedSource.error"
|
|
retryable
|
|
@retry="refreshSources"
|
|
/>
|
|
|
|
<template v-else>
|
|
<div class="grid gap-2">
|
|
<Label for="repository-search">
|
|
{{
|
|
t(
|
|
'repositorySettings.searchLabel',
|
|
)
|
|
}}
|
|
</Label>
|
|
<div class="relative min-w-0">
|
|
<Search
|
|
class="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2"
|
|
aria-hidden="true"
|
|
/>
|
|
<Input
|
|
id="repository-search"
|
|
v-model="repositorySearch"
|
|
type="search"
|
|
class="pl-9"
|
|
:placeholder="
|
|
t(
|
|
'repositorySettings.searchPlaceholder',
|
|
)
|
|
"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="refreshingSources"
|
|
class="gap-stack grid"
|
|
role="status"
|
|
:aria-label="
|
|
t('repositorySettings.refreshing')
|
|
"
|
|
>
|
|
<Skeleton class="h-16 w-full" />
|
|
<Skeleton class="h-16 w-full" />
|
|
<Skeleton class="h-16 w-full" />
|
|
</div>
|
|
|
|
<FeedbackState
|
|
v-else-if="
|
|
selectedSource.repositories
|
|
.length === 0
|
|
"
|
|
state="empty"
|
|
:title="
|
|
t(
|
|
'repositorySettings.noRepositories',
|
|
)
|
|
"
|
|
:description="
|
|
t(
|
|
'repositorySettings.noRepositoriesDescription',
|
|
)
|
|
"
|
|
/>
|
|
|
|
<FeedbackState
|
|
v-else-if="
|
|
filteredRepositories.length === 0
|
|
"
|
|
state="empty"
|
|
:title="
|
|
t(
|
|
'repositorySettings.noSearchResults',
|
|
)
|
|
"
|
|
:description="
|
|
t(
|
|
'repositorySettings.noSearchResultsDescription',
|
|
{ search: repositorySearch },
|
|
)
|
|
"
|
|
/>
|
|
|
|
<div
|
|
v-else
|
|
class="max-h-72 space-y-2 overflow-y-auto pr-1"
|
|
role="radiogroup"
|
|
:aria-label="
|
|
t(
|
|
'repositorySettings.repositoryLabel',
|
|
)
|
|
"
|
|
>
|
|
<button
|
|
v-for="repository in filteredRepositories"
|
|
:key="
|
|
repository.provider_repository_id
|
|
"
|
|
type="button"
|
|
role="radio"
|
|
:aria-checked="
|
|
repositoryForm.provider_repository_id ===
|
|
repository.provider_repository_id
|
|
"
|
|
:class="[
|
|
'focus-visible:ring-ring/35 flex w-full min-w-0 items-start justify-between gap-3 rounded-lg border p-3 text-left outline-none transition-colors focus-visible:ring-2',
|
|
repositoryForm.provider_repository_id ===
|
|
repository.provider_repository_id
|
|
? 'border-primary/50 bg-accent'
|
|
: 'hover:bg-muted/50',
|
|
]"
|
|
@click="
|
|
selectRepository(
|
|
repository.provider_repository_id,
|
|
)
|
|
"
|
|
>
|
|
<span
|
|
class="gap-stack grid min-w-0"
|
|
>
|
|
<span
|
|
class="min-w-0 break-all font-semibold"
|
|
>
|
|
{{ repository.full_name }}
|
|
</span>
|
|
<span
|
|
class="text-caption text-muted-foreground break-all"
|
|
>
|
|
{{
|
|
repository.repository_url
|
|
}}
|
|
</span>
|
|
</span>
|
|
<Badge
|
|
variant="outline"
|
|
class="shrink-0"
|
|
>
|
|
{{
|
|
repository.visibility_label
|
|
}}
|
|
</Badge>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<InputError
|
|
:message="
|
|
repositoryForm.errors
|
|
.provider_repository_id
|
|
"
|
|
/>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
@click="closeDialog(false)"
|
|
>
|
|
{{ t('common.cancel') }}
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
:disabled="!canSaveRepository"
|
|
:loading="repositoryForm.processing"
|
|
:loading-label="
|
|
t(
|
|
dialogMode === 'edit'
|
|
? 'repositorySettings.saving'
|
|
: 'repositorySettings.linking',
|
|
)
|
|
"
|
|
>
|
|
{{
|
|
t(
|
|
dialogMode === 'edit'
|
|
? 'repositorySettings.save'
|
|
: 'repositorySettings.linkAction',
|
|
)
|
|
}}
|
|
</Button>
|
|
</DialogFooter>
|
|
</fieldset>
|
|
</form>
|
|
|
|
<form
|
|
v-else
|
|
class="gap-column grid min-w-0"
|
|
:aria-busy="removalForm.processing"
|
|
@submit.prevent="removeRepository"
|
|
>
|
|
<div
|
|
class="bg-destructive/8 border-destructive/25 text-body grid min-w-0 gap-2 rounded-lg border p-4"
|
|
>
|
|
<p class="break-all font-semibold">
|
|
{{ selectedConnection?.repository_full_name }}
|
|
</p>
|
|
<p class="text-muted-foreground break-all">
|
|
{{ selectedConnection?.base_url }}
|
|
</p>
|
|
</div>
|
|
<FormFeedback
|
|
:state="removalProtection.state.value"
|
|
:title="
|
|
removalProtection.responseStatus.value === 403
|
|
? t('repositorySettings.forbidden')
|
|
: removalForm.processing
|
|
? t('repositorySettings.removing')
|
|
: undefined
|
|
"
|
|
:description="
|
|
removalProtection.responseStatus.value === 403
|
|
? t(
|
|
'repositorySettings.forbiddenDescription',
|
|
)
|
|
: undefined
|
|
"
|
|
:retryable="
|
|
removalProtection.responseStatus.value !== 403
|
|
"
|
|
@retry="removeRepository"
|
|
/>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
:disabled="removalForm.processing"
|
|
@click="closeDialog(false)"
|
|
>
|
|
{{ t('common.cancel') }}
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
variant="destructive"
|
|
:loading="removalForm.processing"
|
|
:loading-label="
|
|
t('repositorySettings.removing')
|
|
"
|
|
>
|
|
{{ t('repositorySettings.removeTitle') }}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</SettingsLayout>
|
|
</AppLayout>
|
|
</template>
|