Add template switch and reset flows for existing projects

This commit is contained in:
2026-09-08 10:25:52 +00:00
committed by Twyco
parent b8a1d65122
commit 49e887753c
10 changed files with 522 additions and 2 deletions
@@ -43,14 +43,70 @@ const breadcrumbs: BreadcrumbItem[] = [
},
];
const switchTemplateForm = useForm({
confirmation_key: '',
source_template_id: props.sourceTemplates.find(
(template) => template.id !== props.project.project_template_id,
)?.id,
});
const resetTemplateForm = useForm({
confirmation_key: '',
});
const deleteForm = useForm({
confirmation_key: '',
});
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,
);
const isResetConfirmationValid = computed(
() => resetTemplateForm.confirmation_key === props.project.key,
);
const isDeleteConfirmationValid = computed(
() => deleteForm.confirmation_key === props.project.key,
);
const submitSwitchTemplate = (): void => {
if (!canSwitchTemplate.value) {
return;
}
switchTemplateForm.submit(
ProjectController.switchTemplate(props.project.id),
{
preserveScroll: true,
},
);
};
const submitResetTemplate = (): void => {
if (!isResetConfirmationValid.value) {
return;
}
resetTemplateForm.submit(ProjectController.resetTemplate(props.project.id), {
preserveScroll: true,
});
};
const submitDelete = (): void => {
if (!isDeleteConfirmationValid.value) {
return;
@@ -152,6 +208,139 @@ const submitDelete = (): void => {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Template baseline</CardTitle>
<CardDescription>
Switch this project to another published template or reset it back to the currently linked template baseline. Both actions can overwrite local configuration.
</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 or resetting.'
: '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">
Template changes can replace board workflow, ticket metadata, repositories, and notifications for copied areas.
</p>
<p class="text-muted-foreground mt-2 text-sm">
Confirm each action with the exact project key to avoid accidental overwrites.
</p>
</div>
</div>
<div class="grid gap-6 xl:grid-cols-2">
<form class="space-y-4 rounded-xl border p-4" @submit.prevent="submitSwitchTemplate">
<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>
<div class="grid gap-2">
<Label for="source_template_id">Published template</Label>
<select
id="source_template_id"
v-model="switchTemplateForm.source_template_id"
:disabled="switchTemplateForm.processing || availableSwitchTemplates.length === 0"
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 :value="undefined" disabled>
Select a template
</option>
<option
v-for="template in availableSwitchTemplates"
:key="template.id"
:value="template.id"
>
{{ template.name }}
</option>
</select>
<InputError :message="switchTemplateForm.errors.source_template_id" />
</div>
<div class="grid gap-2">
<Label for="switch_confirmation_key">Confirmation key</Label>
<Input
id="switch_confirmation_key"
v-model="switchTemplateForm.confirmation_key"
:disabled="switchTemplateForm.processing"
placeholder="Type the project key exactly"
/>
<InputError :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>
<form class="space-y-4 rounded-xl border p-4" @submit.prevent="submitResetTemplate">
<div class="space-y-1">
<h3 class="text-sm font-semibold">Reset to current template baseline</h3>
<p class="text-muted-foreground text-sm">
Re-apply the currently linked template and clear local divergence inside copied areas.
</p>
</div>
<div class="grid gap-2">
<Label for="reset_confirmation_key">Confirmation key</Label>
<Input
id="reset_confirmation_key"
v-model="resetTemplateForm.confirmation_key"
:disabled="resetTemplateForm.processing || !currentTemplate"
placeholder="Type the project key exactly"
/>
<InputError :message="resetTemplateForm.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 restores the current template baseline for copied areas and rewinds project-level overrides there.
</p>
<Button
type="submit"
variant="outline"
:disabled="resetTemplateForm.processing || !isResetConfirmationValid || !currentTemplate"
>
Reset to baseline
</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"
>
@@ -1,7 +1,11 @@
import type { ProjectTemplateModel } from '@/types/models/project-template-model';
export type ProjectModel = {
id: number;
name: string;
key: string;
project_template_id?: number | null;
project_template?: ProjectTemplateModel | null;
show_author: boolean;
show_story_points: boolean;
created_at: string;
@@ -9,6 +9,7 @@ export type ProjectTemplateModel = {
name: string;
slug: string;
copy_areas: string[];
configuration_snapshot: Record<string, unknown>;
created_at: string | null;
updated_at: string | null;
};
+5
View File
@@ -69,8 +69,11 @@ export type ProjectTemplateStateArea = {
export type ProjectTemplateState = {
effectiveConfiguration: Record<string, unknown>;
inheritedAreas: ProjectTemplateStateArea[];
inheritedConfiguration?: Record<string, unknown>;
inheritsFromTemplate: boolean;
overriddenAreas: ProjectTemplateStateArea[];
overriddenConfiguration?: Record<string, unknown>;
templateConfiguration?: Record<string, unknown>;
templateId: number;
templateName: string;
templateSlug: string;
@@ -117,7 +120,9 @@ export type ProjectBoardSettingsPageProps = {
export type ProjectGeneralSettingsPageProps = {
project: ProjectModel;
sourceTemplates: ProjectTemplateModel[];
status?: string | null;
templateState?: ProjectTemplateState | null;
};
export type ProjectBoardPageProps = {