Files
KanbanGit/resources/js/pages/projects/settings/Repositories.vue
T

662 lines
29 KiB
Vue

<script setup lang="ts">
import { Form, Head, Link, useForm } from '@inertiajs/vue3';
import { computed, watch } from 'vue';
import ProjectRepositorySettingsController from '@/actions/App/Http/Controllers/ProjectRepositorySettingsController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import AppLayout from '@/layouts/AppLayout.vue';
import { edit as editGitConnections } from '@/routes/git-connections/index';
import { index, show } from '@/routes/projects/index';
import {
board as boardSettings,
metadata as metadataSettings,
repositories as repositorySettings,
} from '@/routes/projects/settings';
import type { AvailableGitRepositorySourceModel } from '@/types/models/available-git-repository-source-model';
import type { RepositoryConnectionModel } from '@/types/models/repository-connection-model';
import type {
BreadcrumbItem,
ProjectRepositorySettingsPageProps,
} from '@/types';
const props = defineProps<ProjectRepositorySettingsPageProps>();
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Projects',
href: index(),
},
{
title: props.project.name,
href: show(props.project.id),
},
{
title: 'Repository settings',
href: repositorySettings(props.project.id),
},
];
const selectableRepositorySources = computed(() =>
props.availableRepositorySources.filter((source) => source.repositories.length > 0),
);
const repositoryConnectionById = computed(
() =>
new Map(
props.repositoryConnections.map((repositoryConnection) => [
repositoryConnection.id,
repositoryConnection,
]),
),
);
const repositoriesForSource = (
userGitConnectionId: number | string,
): AvailableGitRepositorySourceModel['repositories'] => {
const normalizedUserGitConnectionId = Number(userGitConnectionId);
if (Number.isNaN(normalizedUserGitConnectionId)) {
return [];
}
return (
selectableRepositorySources.value.find(
(source) =>
source.user_git_connection_id === normalizedUserGitConnectionId,
)?.repositories ?? []
);
};
const matchingSourceIdForRepositoryConnection = (
repositoryConnection: RepositoryConnectionModel | undefined,
): string => {
if (repositoryConnection === undefined) {
return String(
selectableRepositorySources.value[0]?.user_git_connection_id ?? '',
);
}
return String(
selectableRepositorySources.value.find(
(source) =>
source.provider === repositoryConnection.provider &&
source.base_url === repositoryConnection.base_url,
)?.user_git_connection_id ??
selectableRepositorySources.value[0]?.user_git_connection_id ??
'',
);
};
const createForm = useForm<{
provider_repository_id: string;
user_git_connection_id: string;
}>({
provider_repository_id: '',
user_git_connection_id: String(
selectableRepositorySources.value[0]?.user_git_connection_id ?? '',
),
});
const updateForm = useForm<{
provider_repository_id: string;
repository_connection_id: string;
user_git_connection_id: string;
}>({
provider_repository_id: '',
repository_connection_id: String(props.repositoryConnections[0]?.id ?? ''),
user_git_connection_id: String(
selectableRepositorySources.value[0]?.user_git_connection_id ?? '',
),
});
const createRepositories = computed(() =>
repositoriesForSource(createForm.user_git_connection_id),
);
const updateRepositories = computed(() =>
repositoriesForSource(updateForm.user_git_connection_id),
);
watch(
() => createForm.user_git_connection_id,
() => {
const repositories = createRepositories.value;
if (
repositories.some(
(repository) =>
repository.provider_repository_id ===
createForm.provider_repository_id,
)
) {
return;
}
createForm.provider_repository_id =
repositories[0]?.provider_repository_id ?? '';
},
{ immediate: true },
);
watch(
() => updateForm.repository_connection_id,
(repositoryConnectionId) => {
const repositoryConnection = repositoryConnectionById.value.get(
Number(repositoryConnectionId),
);
updateForm.user_git_connection_id =
matchingSourceIdForRepositoryConnection(repositoryConnection);
},
{ immediate: true },
);
watch(
[
() => updateForm.user_git_connection_id,
() => updateForm.repository_connection_id,
],
() => {
const repositories = updateRepositories.value;
const repositoryConnection = repositoryConnectionById.value.get(
Number(updateForm.repository_connection_id),
);
const matchingRepository = repositories.find(
(repository) =>
repository.provider_repository_id ===
repositoryConnection?.provider_repository_id,
);
updateForm.provider_repository_id =
matchingRepository?.provider_repository_id ??
repositories[0]?.provider_repository_id ??
'';
},
{ immediate: true },
);
const submitCreate = (): void => {
createForm.submit(
ProjectRepositorySettingsController.store(props.project.id),
{
preserveScroll: true,
},
);
};
const submitUpdate = (): void => {
if (updateForm.repository_connection_id === '') {
return;
}
updateForm.submit(
ProjectRepositorySettingsController.update([
props.project.id,
updateForm.repository_connection_id,
]),
{
preserveScroll: true,
},
);
};
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head :title="`${project.name} repository settings`" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
<div class="space-y-3">
<Badge variant="outline" class="px-3 py-1">
{{ project.key }}
</Badge>
<Heading
:title="`${project.name} repository settings`"
description="Link project repositories through your personal Git connections so tickets can later discover matching branches and pull requests."
/>
</div>
<div class="flex flex-wrap gap-3">
<Button as-child variant="outline">
<Link :href="boardSettings(project.id)">
Board settings
</Link>
</Button>
<Button as-child variant="outline">
<Link :href="metadataSettings(project.id)">
Ticket settings
</Link>
</Button>
<Button as-child variant="outline">
<Link :href="show(project.id)">Back to project</Link>
</Button>
</div>
</div>
<div
v-if="status"
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
>
{{ status }}
</div>
<Card>
<CardHeader>
<CardTitle>Personal Git access</CardTitle>
<CardDescription>
Project repository links are shared across the project,
but every user needs their own Git connection to open or
sync private repository data.
</CardDescription>
</CardHeader>
<CardContent class="flex flex-wrap items-center gap-3">
<Button as-child variant="outline">
<Link :href="editGitConnections()">
Open Git connections
</Link>
</Button>
<p class="text-sm text-muted-foreground">
Without a matching personal connection, KanbanGit only
shows the repository name and links you to your user
settings.
</p>
</CardContent>
</Card>
<div class="grid gap-6 xl:grid-cols-[24rem_minmax(0,1fr)]">
<div class="space-y-6">
<Card>
<CardHeader>
<CardTitle>Link repository</CardTitle>
<CardDescription>
Choose one of your personal Git connections and
link a visible repository to this project.
</CardDescription>
</CardHeader>
<CardContent>
<div
v-if="selectableRepositorySources.length === 0"
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
>
Add a personal Git connection first, or fix an
existing connection that currently cannot load
repositories.
</div>
<form
v-else
class="space-y-4"
@submit.prevent="submitCreate"
>
<div class="grid gap-2">
<Label for="create-user-git-connection">
Personal Git connection
</Label>
<select
id="create-user-git-connection"
v-model="createForm.user_git_connection_id"
class="rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<option
v-for="source in selectableRepositorySources"
:key="source.user_git_connection_id"
:value="source.user_git_connection_id"
>
{{
`${source.provider_label} · ${source.account_login} @ ${source.base_url}`
}}
</option>
</select>
<InputError
:message="
createForm.errors.user_git_connection_id
"
/>
</div>
<div class="grid gap-2">
<Label for="create-provider-repository-id">
Repository
</Label>
<select
id="create-provider-repository-id"
v-model="
createForm.provider_repository_id
"
class="rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<option
v-for="repository in createRepositories"
:key="
repository.provider_repository_id
"
:value="
repository.provider_repository_id
"
>
{{
`${repository.full_name} (${repository.visibility_label})`
}}
</option>
</select>
<InputError
:message="
createForm.errors.provider_repository_id
"
/>
</div>
<Button
type="submit"
:disabled="createForm.processing"
>
Link repository
</Button>
</form>
</CardContent>
</Card>
<Card
v-if="
repositoryConnections.length > 0 &&
selectableRepositorySources.length > 0
"
>
<CardHeader>
<CardTitle>Relink repository</CardTitle>
<CardDescription>
Update an existing project repository
connection by choosing a different repository
from one of your personal Git connections.
</CardDescription>
</CardHeader>
<CardContent>
<form
class="space-y-4"
@submit.prevent="submitUpdate"
>
<div class="grid gap-2">
<Label for="update-repository-connection">
Linked repository
</Label>
<select
id="update-repository-connection"
v-model="
updateForm.repository_connection_id
"
class="rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<option
v-for="repositoryConnection in repositoryConnections"
:key="repositoryConnection.id"
:value="repositoryConnection.id"
>
{{
repositoryConnection.repository_full_name
}}
</option>
</select>
</div>
<div class="grid gap-2">
<Label for="update-user-git-connection">
Personal Git connection
</Label>
<select
id="update-user-git-connection"
v-model="updateForm.user_git_connection_id"
class="rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<option
v-for="source in selectableRepositorySources"
:key="source.user_git_connection_id"
:value="source.user_git_connection_id"
>
{{
`${source.provider_label} · ${source.account_login} @ ${source.base_url}`
}}
</option>
</select>
<InputError
:message="
updateForm.errors.user_git_connection_id
"
/>
</div>
<div class="grid gap-2">
<Label for="update-provider-repository-id">
Repository
</Label>
<select
id="update-provider-repository-id"
v-model="
updateForm.provider_repository_id
"
class="rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<option
v-for="repository in updateRepositories"
:key="
repository.provider_repository_id
"
:value="
repository.provider_repository_id
"
>
{{
`${repository.full_name} (${repository.visibility_label})`
}}
</option>
</select>
<InputError
:message="
updateForm.errors.provider_repository_id
"
/>
</div>
<Button
type="submit"
variant="outline"
:disabled="updateForm.processing"
>
Save repository change
</Button>
</form>
</CardContent>
</Card>
</div>
<div class="space-y-6">
<Card class="gap-0 overflow-hidden">
<CardHeader class="border-b">
<CardTitle>Linked repositories</CardTitle>
<CardDescription>
Project repositories are shared, but access to
private data depends on each viewer's personal
Git connection.
</CardDescription>
</CardHeader>
<CardContent class="space-y-4 p-6">
<div
v-if="repositoryConnections.length === 0"
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
>
No repositories are linked to this project yet.
</div>
<div
v-for="repositoryConnection in repositoryConnections"
:key="repositoryConnection.id"
class="rounded-xl border border-border p-4"
>
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
<div class="space-y-2">
<div class="flex flex-wrap items-center gap-2">
<Badge>
{{
repositoryConnection.provider_label
}}
</Badge>
<Badge variant="secondary">
{{ repositoryConnection.base_url }}
</Badge>
</div>
<div>
<p class="font-medium">
{{
repositoryConnection.repository_full_name
}}
</p>
<p class="text-sm text-muted-foreground">
{{
repositoryConnection.repository_name
}}
by
{{
repositoryConnection.owner_name
}}
</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<Button
v-if="
repositoryConnection.has_personal_access
"
as-child
variant="outline"
>
<a
:href="
repositoryConnection.repository_url
"
target="_blank"
rel="noreferrer"
>
Open repository
</a>
</Button>
<Button
v-else
as-child
variant="outline"
>
<Link :href="editGitConnections()">
Connect Git access
</Link>
</Button>
</div>
</div>
<p
v-if="!repositoryConnection.has_personal_access"
class="mt-3 text-sm text-muted-foreground"
>
This project link is visible, but you need a
matching Git connection in your user
settings before KanbanGit can open or query
this repository for you.
</p>
<div class="mt-4 flex justify-end">
<Form
v-bind="
ProjectRepositorySettingsController.destroy.form(
[
project.id,
repositoryConnection.id,
],
)
"
v-slot="{ processing }"
>
<Button
type="submit"
variant="destructive"
:disabled="processing"
>
Remove
</Button>
</Form>
</div>
</div>
</CardContent>
</Card>
<Card
v-if="availableRepositorySources.length > 0"
class="gap-0 overflow-hidden"
>
<CardHeader class="border-b">
<CardTitle>Detected personal sources</CardTitle>
<CardDescription>
KanbanGit loads repositories from your personal
Git connections and offers them for project
linking.
</CardDescription>
</CardHeader>
<CardContent class="space-y-4 p-6">
<div
v-for="source in availableRepositorySources"
:key="source.user_git_connection_id"
class="rounded-xl border border-border p-4"
>
<div class="flex flex-wrap items-center gap-2">
<Badge>{{ source.provider_label }}</Badge>
<span class="text-sm font-medium">
{{ source.account_login }}
</span>
<span class="text-xs text-muted-foreground">
{{ source.base_url }}
</span>
</div>
<p
v-if="source.error"
class="mt-3 text-sm text-amber-700 dark:text-amber-300"
>
{{ source.error }}
</p>
<p
v-else
class="mt-3 text-sm text-muted-foreground"
>
{{ source.repositories.length }} repositories
available through this connection.
</p>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</AppLayout>
</template>