731 lines
32 KiB
Vue
731 lines
32 KiB
Vue
<script setup lang="ts">
|
|
import { Form, Head, Link, useForm, usePage } 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,
|
|
general as generalSettings,
|
|
metadata as metadataSettings,
|
|
notifications as notificationSettings,
|
|
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 page = usePage();
|
|
const appName = String(page.props.name);
|
|
|
|
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,
|
|
Number(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
|
|
v-if="$page.props.auth.permissions.globalAdmin"
|
|
as-child
|
|
variant="outline"
|
|
>
|
|
<Link :href="generalSettings(project.id)">
|
|
General settings
|
|
</Link>
|
|
</Button>
|
|
|
|
<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="notificationSettings(project.id)">
|
|
Notification 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 v-if="templateState?.inheritsFromTemplate">
|
|
<CardHeader>
|
|
<CardTitle>
|
|
Template inheritance: {{ templateState.templateName }}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Repository links can stay inherited from the template or be replaced with project-specific overrides.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent class="flex flex-wrap gap-2">
|
|
<Badge
|
|
v-for="area in templateState.inheritedAreas"
|
|
:key="`inherited-${area.value}`"
|
|
variant="outline"
|
|
>
|
|
{{ area.label }} · inherited
|
|
</Badge>
|
|
<Badge
|
|
v-for="area in templateState.overriddenAreas"
|
|
:key="`overridden-${area.value}`"
|
|
>
|
|
{{ area.label }} · overridden
|
|
</Badge>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<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-muted-foreground text-sm">
|
|
Without a matching personal connection, {{ appName }} 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="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
|
>
|
|
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="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
>
|
|
<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="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
>
|
|
<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="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
>
|
|
<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="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
>
|
|
<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="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
>
|
|
<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="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
|
>
|
|
No repositories are linked to this project yet.
|
|
</div>
|
|
|
|
<div
|
|
v-for="repositoryConnection in repositoryConnections"
|
|
:key="repositoryConnection.id"
|
|
class="border-border rounded-xl 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-muted-foreground text-sm"
|
|
>
|
|
{{
|
|
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="text-muted-foreground mt-3 text-sm"
|
|
>
|
|
This project link is visible, but you need a
|
|
matching Git connection in your user
|
|
settings before {{ appName }} 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>
|
|
{{ appName }} 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="border-border rounded-xl 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-muted-foreground text-xs">
|
|
{{ 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="text-muted-foreground mt-3 text-sm"
|
|
>
|
|
{{ source.repositories.length }}
|
|
repositories available through this
|
|
connection.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AppLayout>
|
|
</template>
|