494 lines
22 KiB
Vue
494 lines
22 KiB
Vue
<script setup lang="ts">
|
|
import { Head, useForm } from '@inertiajs/vue3';
|
|
import {
|
|
GitBranch,
|
|
LockKeyhole,
|
|
Pencil,
|
|
Plus,
|
|
Server,
|
|
Trash2,
|
|
} from 'lucide-vue-next';
|
|
import { computed, ref } from 'vue';
|
|
import {
|
|
destroy,
|
|
store,
|
|
update,
|
|
} from '@/actions/App/Http/Controllers/Settings/GitConnectionController';
|
|
import FeedbackState from '@/components/FeedbackState.vue';
|
|
import FormFeedback from '@/components/FormFeedback.vue';
|
|
import InputError from '@/components/InputError.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 { useFormProtection } from '@/composables/useFormProtection';
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import AppLayout from '@/layouts/AppLayout.vue';
|
|
import SettingsLayout from '@/layouts/settings/Layout.vue';
|
|
import { edit } from '@/routes/git-connections/index';
|
|
import type { BreadcrumbItem, SettingsGitConnectionsPageProps } from '@/types';
|
|
import type { UserGitConnectionModel } from '@/types/models/user-git-connection-model';
|
|
|
|
const props = defineProps<SettingsGitConnectionsPageProps>();
|
|
const { t } = useLocalization();
|
|
const breadcrumbItems = computed<BreadcrumbItem[]>(() => [
|
|
{ title: t('navigation.gitConnections'), href: edit() },
|
|
]);
|
|
const mode = ref<'create' | 'edit' | 'remove'>('create');
|
|
const dialogOpen = ref(false);
|
|
const selectedConnection = ref<UserGitConnectionModel | null>(null);
|
|
// Keep provider credentials out of Inertia's remembered browser history.
|
|
const connectionForm = useForm({
|
|
provider: 'gitea',
|
|
base_url: '',
|
|
api_token: '',
|
|
});
|
|
const connectionProtection = useFormProtection(connectionForm);
|
|
const removalForm = useForm({});
|
|
const removalProtection = useFormProtection(removalForm);
|
|
const processing = computed(
|
|
() => connectionForm.processing || removalForm.processing,
|
|
);
|
|
const dialogTitle = computed(() =>
|
|
t(
|
|
mode.value === 'create'
|
|
? 'gitConnections.add'
|
|
: mode.value === 'edit'
|
|
? 'gitConnections.edit'
|
|
: 'gitConnections.removeTitle',
|
|
),
|
|
);
|
|
const successTitle = computed(() =>
|
|
props.status === 'Git connection was added.'
|
|
? t('gitConnections.added')
|
|
: props.status === 'Git connection was updated.'
|
|
? t('gitConnections.updated')
|
|
: t('gitConnections.removed'),
|
|
);
|
|
|
|
function resetForms(): void {
|
|
connectionForm.defaults({ provider: 'gitea', base_url: '', api_token: '' });
|
|
connectionForm.resetAndClearErrors();
|
|
connectionProtection.clearFeedback();
|
|
removalForm.resetAndClearErrors();
|
|
removalProtection.clearFeedback();
|
|
}
|
|
|
|
function openDialog(
|
|
nextMode: typeof mode.value,
|
|
connection: UserGitConnectionModel | null = null,
|
|
): void {
|
|
resetForms();
|
|
mode.value = nextMode;
|
|
selectedConnection.value = connection;
|
|
connectionForm.defaults({
|
|
provider: connection?.provider ?? 'gitea',
|
|
base_url: connection?.base_url ?? '',
|
|
api_token: '',
|
|
});
|
|
connectionForm.reset();
|
|
dialogOpen.value = true;
|
|
}
|
|
|
|
function closeDialog(open: boolean): void {
|
|
if (open || processing.value || !connectionProtection.confirmLeave()) {
|
|
return;
|
|
}
|
|
dialogOpen.value = false;
|
|
resetForms();
|
|
}
|
|
|
|
function finish(): void {
|
|
resetForms();
|
|
dialogOpen.value = false;
|
|
}
|
|
|
|
function saveConnection(): void {
|
|
const target =
|
|
mode.value === 'edit' && selectedConnection.value
|
|
? update(selectedConnection.value.id)
|
|
: store();
|
|
connectionProtection.submit(target, {
|
|
preserveScroll: true,
|
|
onSuccess: finish,
|
|
});
|
|
}
|
|
|
|
function removeConnection(): void {
|
|
if (!selectedConnection.value) {
|
|
return;
|
|
}
|
|
removalProtection.submit(destroy(selectedConnection.value.id), {
|
|
preserveScroll: true,
|
|
onSuccess: finish,
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout :breadcrumbs="breadcrumbItems">
|
|
<Head :title="t('navigation.gitConnections')" />
|
|
<SettingsLayout>
|
|
<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('navigation.gitConnections') }}
|
|
</h2>
|
|
<p class="text-muted-foreground">
|
|
{{ t('gitConnections.description') }}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
class="shrink-0 self-start"
|
|
@click="openDialog('create')"
|
|
>
|
|
<Plus aria-hidden="true" />{{
|
|
t('gitConnections.add')
|
|
}}
|
|
</Button>
|
|
</div>
|
|
<div
|
|
class="bg-muted/45 text-caption text-muted-foreground flex items-start gap-2 rounded-lg border p-3"
|
|
>
|
|
<LockKeyhole
|
|
class="text-primary mt-0.5 size-4 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<p>{{ t('gitConnections.privacy') }}</p>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent class="gap-column grid min-w-0 p-5 sm:p-6">
|
|
<FeedbackState
|
|
v-if="status"
|
|
state="success"
|
|
:title="successTitle"
|
|
/>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<h3 class="font-semibold">
|
|
{{ t('gitConnections.saved') }}
|
|
</h3>
|
|
<Badge variant="secondary">{{
|
|
gitConnections.length
|
|
}}</Badge>
|
|
</div>
|
|
<FeedbackState
|
|
v-if="gitConnections.length === 0"
|
|
state="empty"
|
|
:title="t('gitConnections.empty')"
|
|
:description="t('gitConnections.emptyDescription')"
|
|
/>
|
|
<ul
|
|
v-else
|
|
class="gap-column grid min-w-0"
|
|
:aria-label="t('gitConnections.saved')"
|
|
>
|
|
<li
|
|
v-for="connection in gitConnections"
|
|
:key="connection.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"
|
|
>
|
|
{{ connection.account_login }}
|
|
</h4>
|
|
<Badge variant="secondary">{{
|
|
connection.provider_label
|
|
}}</Badge>
|
|
</div>
|
|
<p
|
|
class="text-caption text-muted-foreground break-words"
|
|
>
|
|
{{
|
|
connection.account_name ??
|
|
t('gitConnections.unnamedAccount')
|
|
}}
|
|
</p>
|
|
<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">{{
|
|
connection.base_url
|
|
}}</span>
|
|
</div>
|
|
<p
|
|
class="text-caption text-muted-foreground break-all"
|
|
>
|
|
{{
|
|
t('gitConnections.remoteId', {
|
|
id: connection.provider_user_id,
|
|
})
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="flex flex-wrap gap-2 border-t pt-3 sm:justify-end"
|
|
>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
:aria-label="
|
|
t('gitConnections.editAccount', {
|
|
account: connection.account_login,
|
|
server: connection.base_url,
|
|
})
|
|
"
|
|
@click="openDialog('edit', connection)"
|
|
>
|
|
<Pencil aria-hidden="true" />{{
|
|
t('gitConnections.editAction')
|
|
}}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
:aria-label="
|
|
t('gitConnections.removeAccount', {
|
|
account: connection.account_login,
|
|
server: connection.base_url,
|
|
})
|
|
"
|
|
@click="openDialog('remove', connection)"
|
|
>
|
|
<Trash2 aria-hidden="true" />{{
|
|
t('gitConnections.remove')
|
|
}}
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</CardContent>
|
|
</Card>
|
|
<Dialog :open="dialogOpen" @update:open="closeDialog">
|
|
<DialogContent
|
|
:show-close-button="!processing"
|
|
@escape-key-down="processing && $event.preventDefault()"
|
|
@interact-outside="processing && $event.preventDefault()"
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle>{{ dialogTitle }}</DialogTitle>
|
|
<DialogDescription>{{
|
|
t(
|
|
mode === 'remove'
|
|
? 'gitConnections.removeDescription'
|
|
: mode === 'edit'
|
|
? 'gitConnections.editDescription'
|
|
: 'gitConnections.addDescription',
|
|
)
|
|
}}</DialogDescription>
|
|
</DialogHeader>
|
|
<form
|
|
v-if="mode !== 'remove'"
|
|
class="gap-column grid min-w-0"
|
|
:aria-busy="connectionForm.processing"
|
|
@submit.prevent="saveConnection"
|
|
>
|
|
<FormFeedback
|
|
:state="connectionProtection.state.value"
|
|
:title="
|
|
connectionProtection.responseStatus.value ===
|
|
403
|
|
? t('gitConnections.forbidden')
|
|
: connectionForm.processing
|
|
? t('gitConnections.verifying')
|
|
: undefined
|
|
"
|
|
:description="
|
|
connectionProtection.responseStatus.value ===
|
|
403
|
|
? t('gitConnections.forbiddenDescription')
|
|
: undefined
|
|
"
|
|
:retryable="
|
|
connectionProtection.responseStatus.value !==
|
|
403
|
|
"
|
|
@retry="saveConnection"
|
|
/>
|
|
<fieldset
|
|
class="gap-column grid min-w-0"
|
|
:disabled="connectionForm.processing"
|
|
>
|
|
<legend class="sr-only">{{ dialogTitle }}</legend>
|
|
<Badge
|
|
variant="secondary"
|
|
class="justify-self-start"
|
|
>{{ t('gitConnections.gitea') }}</Badge
|
|
>
|
|
<InputError
|
|
:message="connectionForm.errors.provider"
|
|
/>
|
|
<div class="grid gap-2">
|
|
<Label for="git-base-url">{{
|
|
t('gitConnections.baseUrl')
|
|
}}</Label>
|
|
<Input
|
|
id="git-base-url"
|
|
v-model="connectionForm.base_url"
|
|
name="base_url"
|
|
type="url"
|
|
required
|
|
placeholder="https://git.example.com"
|
|
autocomplete="url"
|
|
:aria-invalid="
|
|
!!connectionForm.errors.base_url
|
|
"
|
|
aria-describedby="git-base-url-error"
|
|
/>
|
|
<InputError
|
|
id="git-base-url-error"
|
|
:message="connectionForm.errors.base_url"
|
|
/>
|
|
</div>
|
|
<div class="grid gap-2">
|
|
<Label for="git-token">{{
|
|
t(
|
|
mode === 'edit'
|
|
? 'gitConnections.newToken'
|
|
: 'gitConnections.token',
|
|
)
|
|
}}</Label>
|
|
<Input
|
|
id="git-token"
|
|
v-model="connectionForm.api_token"
|
|
name="api_token"
|
|
type="password"
|
|
required
|
|
autocomplete="new-password"
|
|
:placeholder="
|
|
t('gitConnections.tokenPlaceholder')
|
|
"
|
|
:aria-invalid="
|
|
!!connectionForm.errors.api_token
|
|
"
|
|
aria-describedby="git-token-help git-token-error"
|
|
/>
|
|
<p
|
|
id="git-token-help"
|
|
class="text-caption text-muted-foreground"
|
|
>
|
|
{{ t('gitConnections.tokenHelp') }}
|
|
</p>
|
|
<InputError
|
|
id="git-token-error"
|
|
:message="connectionForm.errors.api_token"
|
|
/>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
@click="closeDialog(false)"
|
|
>{{ t('common.cancel') }}</Button
|
|
>
|
|
<Button type="submit">{{
|
|
t(
|
|
connectionForm.processing
|
|
? 'gitConnections.verifying'
|
|
: mode === 'edit'
|
|
? 'gitConnections.save'
|
|
: 'gitConnections.add',
|
|
)
|
|
}}</Button>
|
|
</DialogFooter>
|
|
</fieldset>
|
|
</form>
|
|
<form
|
|
v-else
|
|
class="gap-column grid min-w-0"
|
|
:aria-busy="removalForm.processing"
|
|
@submit.prevent="removeConnection"
|
|
>
|
|
<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?.account_login }}
|
|
</p>
|
|
<p class="text-muted-foreground break-all">
|
|
{{ selectedConnection?.base_url }}
|
|
</p>
|
|
</div>
|
|
<FormFeedback
|
|
:state="removalProtection.state.value"
|
|
:title="
|
|
removalProtection.responseStatus.value === 403
|
|
? t('gitConnections.forbidden')
|
|
: removalForm.processing
|
|
? t('gitConnections.removing')
|
|
: undefined
|
|
"
|
|
:description="
|
|
removalProtection.responseStatus.value === 403
|
|
? t('gitConnections.forbiddenDescription')
|
|
: undefined
|
|
"
|
|
:retryable="
|
|
removalProtection.responseStatus.value !== 403
|
|
"
|
|
@retry="removeConnection"
|
|
/>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
:disabled="removalForm.processing"
|
|
@click="closeDialog(false)"
|
|
>{{ t('common.cancel') }}</Button
|
|
>
|
|
<Button
|
|
type="submit"
|
|
variant="destructive"
|
|
:disabled="removalForm.processing"
|
|
>{{
|
|
t(
|
|
removalForm.processing
|
|
? 'gitConnections.removing'
|
|
: 'gitConnections.removeTitle',
|
|
)
|
|
}}</Button
|
|
>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</SettingsLayout>
|
|
</AppLayout>
|
|
</template>
|