feat(KAN-39): rebuild general project settings and inheritance
This commit is contained in:
@@ -1,281 +1,316 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import ProjectController from '@/actions/App/Http/Controllers/ProjectController';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { Layers3, Trash2 } from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
destroy,
|
||||
switchTemplate,
|
||||
} from '@/actions/App/Http/Controllers/ProjectController';
|
||||
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,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
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/projects/SettingsLayout.vue';
|
||||
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 { general } from '@/routes/projects/settings';
|
||||
import type { BreadcrumbItem, ProjectGeneralSettingsPageProps } from '@/types';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<ProjectGeneralSettingsPageProps>();
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Projects',
|
||||
href: index(),
|
||||
},
|
||||
{
|
||||
title: props.project.name,
|
||||
href: show(props.project.id),
|
||||
},
|
||||
{
|
||||
title: 'General settings',
|
||||
href: generalSettings(props.project.id),
|
||||
},
|
||||
];
|
||||
|
||||
const switchTemplateForm = useForm({
|
||||
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.general'), href: general(props.project.id) },
|
||||
]);
|
||||
const switchTemplateForm = useForm<{
|
||||
confirmation_key: string;
|
||||
source_template_id: number | undefined;
|
||||
}>({
|
||||
confirmation_key: '',
|
||||
source_template_id: props.sourceTemplates.find(
|
||||
(template) => template.id !== props.project.project_template_id,
|
||||
)?.id,
|
||||
source_template_id: undefined,
|
||||
});
|
||||
|
||||
const deleteForm = useForm({
|
||||
confirmation_key: '',
|
||||
});
|
||||
|
||||
const deleteForm = useForm({ confirmation_key: '' });
|
||||
const switchProtection = useFormProtection(switchTemplateForm);
|
||||
const deleteProtection = useFormProtection(deleteForm);
|
||||
const processing = computed(
|
||||
() => switchTemplateForm.processing || deleteForm.processing,
|
||||
);
|
||||
const availableSwitchTemplates = computed(() =>
|
||||
props.sourceTemplates.filter(
|
||||
(template) => template.id !== props.project.project_template_id,
|
||||
),
|
||||
);
|
||||
|
||||
const currentTemplate = computed(() => props.project.project_template ?? null);
|
||||
|
||||
const isSwitchConfirmationValid = computed(
|
||||
() => switchTemplateForm.confirmation_key === props.project.key,
|
||||
);
|
||||
|
||||
const canSwitchTemplate = computed(
|
||||
() =>
|
||||
isSwitchConfirmationValid.value &&
|
||||
switchTemplateForm.source_template_id !== undefined,
|
||||
switchTemplateForm.confirmation_key === props.project.key &&
|
||||
availableSwitchTemplates.value.some(
|
||||
(template) => template.id === switchTemplateForm.source_template_id,
|
||||
),
|
||||
);
|
||||
|
||||
const isDeleteConfirmationValid = computed(
|
||||
() => deleteForm.confirmation_key === props.project.key,
|
||||
);
|
||||
const inheritanceAreas = computed(() =>
|
||||
[
|
||||
...(props.templateState?.inheritedAreas ?? []),
|
||||
...(props.templateState?.overriddenAreas ?? []),
|
||||
].map((area) => area.value),
|
||||
);
|
||||
const settingsSource = computed(() => {
|
||||
if (!props.templateState?.inheritsFromTemplate) {
|
||||
return 'projectSettings.local';
|
||||
}
|
||||
if (
|
||||
props.templateState.inheritedAreas.some(
|
||||
(area) => area.value === 'project_settings',
|
||||
)
|
||||
) {
|
||||
return 'projectSettings.inherited';
|
||||
}
|
||||
return props.templateState.overriddenAreas.some(
|
||||
(area) => area.value === 'project_settings',
|
||||
)
|
||||
? 'projectSettings.override'
|
||||
: 'projectSettings.local';
|
||||
});
|
||||
|
||||
const submitSwitchTemplate = (): void => {
|
||||
if (!canSwitchTemplate.value) {
|
||||
function submitSwitchTemplate(): void {
|
||||
if (!canSwitchTemplate.value || processing.value) {
|
||||
return;
|
||||
}
|
||||
switchProtection.submit(switchTemplate(props.project.id), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => switchTemplateForm.resetAndClearErrors(),
|
||||
});
|
||||
}
|
||||
|
||||
switchTemplateForm.post(
|
||||
ProjectController.switchTemplate.url(props.project.id),
|
||||
{
|
||||
preserveScroll: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const submitDelete = (): void => {
|
||||
if (!isDeleteConfirmationValid.value) {
|
||||
function submitDelete(): void {
|
||||
if (!isDeleteConfirmationValid.value || processing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteForm.delete(ProjectController.destroy.url(props.project.id), {
|
||||
deleteProtection.submit(destroy(props.project.id), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<Head :title="`${project.name} general 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} general settings`"
|
||||
description="Review the core project identity and manage irreversible administrative actions."
|
||||
/>
|
||||
</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="repositorySettings(project.id)">
|
||||
Repository 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
|
||||
<Head
|
||||
:title="t('projectSettings.pageTitle', { project: project.name })"
|
||||
/>
|
||||
<SettingsLayout :project-name="project.name" :project-key="project.key">
|
||||
<FeedbackState
|
||||
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"
|
||||
state="success"
|
||||
:title="t('projectSettings.updated')"
|
||||
/>
|
||||
<Card
|
||||
class="min-w-0 gap-0 overflow-hidden rounded-xl py-0 shadow-none"
|
||||
>
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Project identity</CardTitle>
|
||||
<CardDescription>
|
||||
These values define the project across tickets, boards,
|
||||
and repository matching.
|
||||
</CardDescription>
|
||||
<CardHeader class="gap-stack grid-cols-1 border-b p-5 sm:p-6">
|
||||
<h2 class="text-title">{{ t('navigation.general') }}</h2>
|
||||
<p class="text-muted-foreground">
|
||||
{{ t('projectSettings.generalDescription') }}
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="grid gap-4 md:grid-cols-2">
|
||||
<div class="border-border/70 rounded-xl border p-4">
|
||||
<p
|
||||
class="text-muted-foreground text-xs font-semibold uppercase tracking-[0.18em]"
|
||||
>
|
||||
Project name
|
||||
</p>
|
||||
<p class="text-foreground mt-2 text-sm font-semibold">
|
||||
{{ project.name }}
|
||||
</p>
|
||||
<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('projectSettings.identity') }}
|
||||
</h3>
|
||||
<Badge variant="secondary">{{
|
||||
t('projectSettings.local')
|
||||
}}</Badge>
|
||||
</div>
|
||||
|
||||
<div class="border-border/70 rounded-xl border p-4">
|
||||
<p
|
||||
class="text-muted-foreground text-xs font-semibold uppercase tracking-[0.18em]"
|
||||
<dl class="grid min-w-0 gap-4 sm:grid-cols-2">
|
||||
<div
|
||||
class="bg-muted/35 gap-stack grid min-w-0 rounded-lg border p-4"
|
||||
>
|
||||
<dt class="text-caption text-muted-foreground">
|
||||
{{ t('projectSettings.name') }}
|
||||
</dt>
|
||||
<dd class="break-words font-semibold">
|
||||
{{ project.name }}
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
class="bg-muted/35 gap-stack grid min-w-0 rounded-lg border p-4"
|
||||
>
|
||||
<dt class="text-caption text-muted-foreground">
|
||||
{{ t('projectSettings.key') }}
|
||||
</dt>
|
||||
<dd class="break-all font-semibold">
|
||||
{{ project.key }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p class="text-caption text-muted-foreground">
|
||||
{{ t('projectSettings.identityDescription') }}
|
||||
</p>
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 border-t pt-4"
|
||||
>
|
||||
<h3 class="font-semibold">
|
||||
{{ t('projectSettings.display') }}
|
||||
</h3>
|
||||
<Badge
|
||||
:variant="
|
||||
settingsSource === 'projectSettings.inherited'
|
||||
? 'outline'
|
||||
: 'secondary'
|
||||
"
|
||||
>{{ t(settingsSource) }}</Badge
|
||||
>
|
||||
Project key
|
||||
</p>
|
||||
<p class="text-foreground mt-2 text-sm font-semibold">
|
||||
{{ project.key }}
|
||||
</p>
|
||||
</div>
|
||||
<dl class="grid gap-3">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<dt class="text-muted-foreground">
|
||||
{{ t('projectSettings.showAuthor') }}
|
||||
</dt>
|
||||
<dd class="font-medium">
|
||||
{{
|
||||
t(
|
||||
project.show_author
|
||||
? 'projectSettings.enabled'
|
||||
: 'projectSettings.disabled',
|
||||
)
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<dt class="text-muted-foreground">
|
||||
{{ t('projectSettings.showStoryPoints') }}
|
||||
</dt>
|
||||
<dd class="font-medium">
|
||||
{{
|
||||
t(
|
||||
project.show_story_points
|
||||
? 'projectSettings.enabled'
|
||||
: 'projectSettings.disabled',
|
||||
)
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p class="text-caption text-muted-foreground">
|
||||
{{ t('projectSettings.displayDescription') }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Template baseline</CardTitle>
|
||||
<CardDescription>
|
||||
Switch this project to another published template. Use
|
||||
the inheritance panels to reset individual settings to
|
||||
the currently linked template.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-6">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="border-border/70 rounded-xl border p-4">
|
||||
<p
|
||||
class="text-muted-foreground text-xs font-semibold uppercase tracking-[0.18em]"
|
||||
>
|
||||
Current linked template
|
||||
</p>
|
||||
<p
|
||||
class="text-foreground mt-2 text-sm font-semibold"
|
||||
>
|
||||
{{
|
||||
currentTemplate?.name ??
|
||||
'No linked template'
|
||||
}}
|
||||
</p>
|
||||
<p class="text-muted-foreground mt-2 text-sm">
|
||||
{{
|
||||
templateState?.inheritsFromTemplate
|
||||
? 'Inherited and overridden areas stay visible after switching.'
|
||||
: 'This project currently behaves like a standalone baseline.'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="border-border/70 rounded-xl border p-4">
|
||||
<p
|
||||
class="text-muted-foreground text-xs font-semibold uppercase tracking-[0.18em]"
|
||||
>
|
||||
Warning
|
||||
</p>
|
||||
<p class="text-foreground mt-2 text-sm font-medium">
|
||||
Switching templates can replace inherited board
|
||||
workflow, ticket metadata, repositories, and
|
||||
notifications.
|
||||
</p>
|
||||
<p class="text-muted-foreground mt-2 text-sm">
|
||||
Confirm each action with the exact project key
|
||||
to avoid accidental overwrites.
|
||||
</p>
|
||||
</div>
|
||||
<TemplateInheritancePanel
|
||||
:project-id="project.id"
|
||||
:areas="inheritanceAreas"
|
||||
:template-state="templateState"
|
||||
/>
|
||||
<Card
|
||||
class="min-w-0 gap-0 overflow-hidden rounded-xl py-0 shadow-none"
|
||||
>
|
||||
<CardHeader class="gap-stack grid-cols-1 border-b p-5 sm:p-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<Layers3
|
||||
class="text-primary size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2 class="text-title">
|
||||
{{ t('projectSettings.template') }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6">
|
||||
<form
|
||||
class="space-y-4 rounded-xl border p-4"
|
||||
@submit.prevent="submitSwitchTemplate"
|
||||
<p class="text-muted-foreground">
|
||||
{{ t('projectSettings.templateDescription') }}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent class="gap-column grid min-w-0 p-5 sm:p-6">
|
||||
<div
|
||||
class="bg-muted/35 gap-stack grid min-w-0 rounded-lg border p-4"
|
||||
>
|
||||
<p class="text-caption text-muted-foreground">
|
||||
{{ t('projectSettings.currentTemplate') }}
|
||||
</p>
|
||||
<p class="break-words font-semibold">
|
||||
{{
|
||||
currentTemplate?.name ??
|
||||
t('projectSettings.noTemplate')
|
||||
}}
|
||||
</p>
|
||||
<Badge variant="outline" class="justify-self-start">{{
|
||||
t(
|
||||
templateState?.inheritsFromTemplate
|
||||
? 'projectSettings.linked'
|
||||
: 'projectSettings.standalone',
|
||||
)
|
||||
}}</Badge>
|
||||
<p
|
||||
v-if="!templateState?.inheritsFromTemplate"
|
||||
class="text-caption text-muted-foreground"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-semibold">
|
||||
Switch to another template
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Applying a different template replaces the
|
||||
copied areas with that template's current
|
||||
baseline.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{ t('projectSettings.standaloneDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
<FeedbackState
|
||||
v-if="availableSwitchTemplates.length === 0"
|
||||
state="empty"
|
||||
:title="t('projectSettings.noAlternatives')"
|
||||
:description="
|
||||
t('projectSettings.noAlternativesDescription')
|
||||
"
|
||||
/>
|
||||
<form
|
||||
v-else
|
||||
class="gap-column grid min-w-0"
|
||||
:aria-busy="switchTemplateForm.processing"
|
||||
@submit.prevent="submitSwitchTemplate"
|
||||
>
|
||||
<FormFeedback
|
||||
:state="switchProtection.state.value"
|
||||
:title="
|
||||
switchTemplateForm.processing
|
||||
? t('projectSettings.switching')
|
||||
: undefined
|
||||
"
|
||||
@retry="submitSwitchTemplate"
|
||||
/>
|
||||
<fieldset
|
||||
class="gap-column grid min-w-0"
|
||||
:disabled="processing"
|
||||
>
|
||||
<legend class="mb-3 font-semibold">
|
||||
{{ t('projectSettings.switch') }}
|
||||
</legend>
|
||||
<p
|
||||
class="border-destructive/25 bg-destructive/8 rounded-lg border p-4"
|
||||
>
|
||||
{{ t('projectSettings.switchWarning') }}
|
||||
</p>
|
||||
<div class="grid gap-2">
|
||||
<Label for="source_template_id"
|
||||
>Published template</Label
|
||||
>
|
||||
<Label for="source_template_id">{{
|
||||
t('projectSettings.publishedTemplate')
|
||||
}}</Label>
|
||||
<select
|
||||
id="source_template_id"
|
||||
v-model="
|
||||
switchTemplateForm.source_template_id
|
||||
"
|
||||
:disabled="
|
||||
switchTemplateForm.processing ||
|
||||
availableSwitchTemplates.length === 0
|
||||
class="border-input bg-background focus-visible:border-ring focus-visible:ring-ring/50 min-h-11 w-full min-w-0 rounded-md border px-3 py-2 text-sm outline-none focus-visible:ring-2"
|
||||
:aria-invalid="
|
||||
!!switchTemplateForm.errors
|
||||
.source_template_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]"
|
||||
aria-describedby="source-template-error"
|
||||
>
|
||||
<option :value="undefined" disabled>
|
||||
Select a template
|
||||
{{
|
||||
t('projectSettings.selectTemplate')
|
||||
}}
|
||||
</option>
|
||||
<option
|
||||
v-for="template in availableSwitchTemplates"
|
||||
@@ -286,120 +321,167 @@ const submitDelete = (): void => {
|
||||
</option>
|
||||
</select>
|
||||
<InputError
|
||||
id="source-template-error"
|
||||
:message="
|
||||
switchTemplateForm.errors
|
||||
.source_template_id
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="switch_confirmation_key"
|
||||
>Confirmation key</Label
|
||||
>
|
||||
<Label for="switch_confirmation_key">{{
|
||||
t('projectSettings.confirmation')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="switch_confirmation_key"
|
||||
v-model="
|
||||
switchTemplateForm.confirmation_key
|
||||
"
|
||||
:disabled="switchTemplateForm.processing"
|
||||
placeholder="Type the project key exactly"
|
||||
autocomplete="off"
|
||||
:placeholder="
|
||||
t(
|
||||
'projectSettings.confirmPlaceholder',
|
||||
{ key: project.key },
|
||||
)
|
||||
"
|
||||
:aria-invalid="
|
||||
!!switchTemplateForm.errors
|
||||
.confirmation_key
|
||||
"
|
||||
aria-describedby="switch-confirm-help switch-confirm-error"
|
||||
/>
|
||||
<p
|
||||
id="switch-confirm-help"
|
||||
class="text-caption text-muted-foreground"
|
||||
>
|
||||
{{
|
||||
t('projectSettings.confirmHelp', {
|
||||
key: project.key,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<InputError
|
||||
id="switch-confirm-error"
|
||||
:message="
|
||||
switchTemplateForm.errors
|
||||
.confirmation_key
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Existing project-specific configuration in
|
||||
copied areas may be replaced or removed.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
:disabled="
|
||||
switchTemplateForm.processing ||
|
||||
!canSwitchTemplate
|
||||
"
|
||||
>
|
||||
Switch template
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
class="border-red-200/80 bg-red-50/60 dark:border-red-900/60 dark:bg-red-950/20"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-red-900 dark:text-red-100">
|
||||
Danger zone
|
||||
</CardTitle>
|
||||
<CardDescription
|
||||
class="text-red-800/80 dark:text-red-200/80"
|
||||
>
|
||||
Deleting this project is permanent. All tickets, board
|
||||
columns, ticket metadata, repository connections, and
|
||||
project permissions will be removed.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-5">
|
||||
<div
|
||||
class="bg-background/80 rounded-xl border border-red-200/80 p-4 dark:border-red-900/60"
|
||||
>
|
||||
<p class="text-foreground text-sm font-medium">
|
||||
Confirm by typing the exact project key:
|
||||
<span class="font-semibold">{{ project.key }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-4" @submit.prevent="submitDelete">
|
||||
<div class="grid gap-2">
|
||||
<Label for="confirmation_key">
|
||||
Confirmation key
|
||||
</Label>
|
||||
<Input
|
||||
id="confirmation_key"
|
||||
v-model="deleteForm.confirmation_key"
|
||||
:disabled="deleteForm.processing"
|
||||
placeholder="Type the project key exactly"
|
||||
/>
|
||||
<InputError
|
||||
:message="deleteForm.errors.confirmation_key"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
:disabled="
|
||||
deleteForm.processing ||
|
||||
!isDeleteConfirmationValid
|
||||
"
|
||||
variant="outline"
|
||||
class="justify-self-start"
|
||||
:disabled="!canSwitchTemplate"
|
||||
>{{
|
||||
t(
|
||||
switchTemplateForm.processing
|
||||
? 'projectSettings.switching'
|
||||
: 'projectSettings.switch',
|
||||
)
|
||||
}}</Button
|
||||
>
|
||||
Delete project
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<Card
|
||||
class="border-destructive/30 min-w-0 gap-0 overflow-hidden rounded-xl py-0 shadow-none"
|
||||
>
|
||||
<CardHeader
|
||||
class="bg-destructive/5 gap-stack grid-cols-1 border-b p-5 sm:p-6"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Trash2
|
||||
class="text-destructive size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2 class="text-title">
|
||||
{{ t('projectSettings.danger') }}
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-muted-foreground">
|
||||
{{ t('projectSettings.deleteDescription') }}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent class="p-5 sm:p-6">
|
||||
<form
|
||||
class="gap-column grid min-w-0"
|
||||
:aria-busy="deleteForm.processing"
|
||||
@submit.prevent="submitDelete"
|
||||
>
|
||||
<FormFeedback
|
||||
:state="deleteProtection.state.value"
|
||||
:title="
|
||||
deleteForm.processing
|
||||
? t('projectSettings.deleting')
|
||||
: undefined
|
||||
"
|
||||
@retry="submitDelete"
|
||||
/>
|
||||
<fieldset
|
||||
class="gap-column grid min-w-0"
|
||||
:disabled="processing"
|
||||
>
|
||||
<legend class="sr-only">
|
||||
{{ t('projectSettings.delete') }}
|
||||
</legend>
|
||||
<p class="text-caption font-medium">
|
||||
{{ t('projectSettings.irreversible') }}
|
||||
</p>
|
||||
<div class="grid gap-2">
|
||||
<Label for="confirmation_key">{{
|
||||
t('projectSettings.confirmation')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="confirmation_key"
|
||||
v-model="deleteForm.confirmation_key"
|
||||
autocomplete="off"
|
||||
:placeholder="
|
||||
t(
|
||||
'projectSettings.confirmPlaceholder',
|
||||
{ key: project.key },
|
||||
)
|
||||
"
|
||||
:aria-invalid="
|
||||
!!deleteForm.errors.confirmation_key
|
||||
"
|
||||
aria-describedby="delete-confirm-help delete-confirm-error"
|
||||
/>
|
||||
<p
|
||||
id="delete-confirm-help"
|
||||
class="text-caption text-muted-foreground"
|
||||
>
|
||||
{{
|
||||
t('projectSettings.confirmHelp', {
|
||||
key: project.key,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<InputError
|
||||
id="delete-confirm-error"
|
||||
:message="
|
||||
deleteForm.errors.confirmation_key
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
class="justify-self-start"
|
||||
:disabled="!isDeleteConfirmationValid"
|
||||
>{{
|
||||
t(
|
||||
deleteForm.processing
|
||||
? 'projectSettings.deleting'
|
||||
: 'projectSettings.delete',
|
||||
)
|
||||
}}</Button
|
||||
>
|
||||
</fieldset>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</SettingsLayout>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user