Add project duplication flow

This commit is contained in:
2026-09-08 10:25:52 +00:00
committed by Twyco
parent 18d19fa473
commit 5abfae2736
8 changed files with 740 additions and 24 deletions
+149 -17
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { Form, Head, Link } from '@inertiajs/vue3';
import { Head, Link, useForm } from '@inertiajs/vue3';
import { computed } from 'vue';
import ProjectController from '@/actions/App/Http/Controllers/ProjectController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
@@ -15,7 +16,9 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import AppLayout from '@/layouts/AppLayout.vue';
import { create, index } from '@/routes/projects/index';
import type { BreadcrumbItem } from '@/types';
import type { BreadcrumbItem, ProjectsCreatePageProps } from '@/types';
const props = defineProps<ProjectsCreatePageProps>();
const breadcrumbs: BreadcrumbItem[] = [
{
@@ -27,6 +30,35 @@ const breadcrumbs: BreadcrumbItem[] = [
href: create(),
},
];
const form = useForm({
copy_areas: [] as string[],
creation_mode: 'scratch',
key: '',
name: '',
source_project_id: props.sourceProjects[0]?.id ? String(props.sourceProjects[0].id) : '',
});
const hasSourceProjects = computed(() => props.sourceProjects.length > 0);
const copyAreasError = computed(() => {
return (
form.errors.copy_areas ??
Object.entries(form.errors).find(([key]) => key.startsWith('copy_areas.'))?.[1] ??
''
);
});
const submit = (): void => {
if (form.creation_mode !== 'duplicate') {
form.source_project_id = '';
form.copy_areas = [];
}
form.post(ProjectController.store(), {
preserveScroll: true,
});
};
</script>
<template>
@@ -36,7 +68,7 @@ const breadcrumbs: BreadcrumbItem[] = [
<div class="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6 p-4">
<Heading
title="Create project"
description="Create a project with a stable uppercase key that will prefix all future ticket keys."
description="Create a fresh project or start from an existing project's settings without copying any tickets."
/>
<Card>
@@ -49,28 +81,63 @@ const breadcrumbs: BreadcrumbItem[] = [
</CardHeader>
<CardContent>
<Form
v-bind="ProjectController.store.form()"
class="space-y-6"
v-slot="{ errors, processing }"
>
<form class="space-y-6" @submit.prevent="submit">
<div class="grid gap-3">
<Label>How do you want to start?</Label>
<div class="grid gap-3 sm:grid-cols-2">
<Label class="flex cursor-pointer items-start gap-3 rounded-xl border p-4">
<input
v-model="form.creation_mode"
type="radio"
value="scratch"
class="mt-1"
/>
<div class="space-y-1">
<div class="font-medium">Create from scratch</div>
<p class="text-muted-foreground text-sm">
Start with the default project setup.
</p>
</div>
</Label>
<Label
class="flex items-start gap-3 rounded-xl border p-4"
:class="hasSourceProjects ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
>
<input
v-model="form.creation_mode"
type="radio"
value="duplicate"
class="mt-1"
:disabled="!hasSourceProjects"
/>
<div class="space-y-1">
<div class="font-medium">Create from existing project</div>
<p class="text-muted-foreground text-sm">
Copy only the settings you explicitly select. Tickets are never duplicated.
</p>
</div>
</Label>
</div>
</div>
<div class="grid gap-2">
<Label for="name">Project name</Label>
<Input
id="name"
name="name"
v-model="form.name"
required
autofocus
placeholder="Kanban foundation"
/>
<InputError :message="errors.name" />
<InputError :message="form.errors.name" />
</div>
<div class="grid gap-2">
<Label for="key">Project key</Label>
<Input
id="key"
name="key"
v-model="form.key"
required
maxlength="5"
placeholder="CORE"
@@ -78,15 +145,80 @@ const breadcrumbs: BreadcrumbItem[] = [
/>
<p class="text-muted-foreground text-sm">
Example ticket keys will look like
<span class="text-foreground font-medium"
>CORE-1</span
>.
<span class="text-foreground font-medium">CORE-1</span>.
</p>
<InputError :message="errors.key" />
<InputError :message="form.errors.key" />
</div>
<div
v-if="form.creation_mode === 'duplicate'"
class="space-y-6 rounded-xl border border-dashed p-4"
>
<div class="grid gap-2">
<Label for="source_project_id">Source project</Label>
<select
id="source_project_id"
v-model="form.source_project_id"
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
:disabled="!hasSourceProjects"
>
<option value="">Select a source project</option>
<option
v-for="sourceProject in sourceProjects"
:key="sourceProject.id"
:value="String(sourceProject.id)"
>
{{ sourceProject.name }} ({{ sourceProject.key }})
</option>
</select>
<p class="text-muted-foreground text-sm">
The new project keeps its own identity, key, and future ticket numbering.
</p>
<InputError :message="form.errors.source_project_id" />
</div>
<div class="grid gap-3">
<div>
<Label>Copy options</Label>
<p class="text-muted-foreground text-sm">
Choose which settings should be copied into the new project.
</p>
</div>
<div class="grid gap-3">
<Label
v-for="copyAreaOption in copyAreaOptions"
:key="copyAreaOption.value"
class="flex cursor-pointer items-start gap-3 rounded-xl border p-4"
>
<input
v-model="form.copy_areas"
type="checkbox"
:value="copyAreaOption.value"
class="mt-1"
/>
<div class="space-y-1">
<div class="font-medium">{{ copyAreaOption.label }}</div>
<p class="text-muted-foreground text-sm">
{{ copyAreaOption.description }}
</p>
</div>
</Label>
</div>
<p class="text-muted-foreground text-sm">
Operational ticket data is always left behind so the new project starts clean.
</p>
<InputError :message="copyAreasError" />
</div>
</div>
<div v-else-if="!hasSourceProjects" class="text-muted-foreground text-sm">
No accessible source projects are available for duplication yet.
</div>
<div class="flex items-center gap-3">
<Button type="submit" :disabled="processing">
<Button type="submit" :disabled="form.processing">
Create project
</Button>
@@ -94,7 +226,7 @@ const breadcrumbs: BreadcrumbItem[] = [
<Link :href="index()">Cancel</Link>
</Button>
</div>
</Form>
</form>
</CardContent>
</Card>
</div>
+11
View File
@@ -32,11 +32,22 @@ export type AdminUserEditPageProps = {
user: UserModel;
};
export type ProjectDuplicateCopyAreaOption = {
description: string;
label: string;
value: string;
};
export type ProjectsIndexPageProps = {
projects: ProjectModel[];
status?: string | null;
};
export type ProjectsCreatePageProps = {
copyAreaOptions: ProjectDuplicateCopyAreaOption[];
sourceProjects: ProjectModel[];
};
export type ProjectShowPageProps = {
canManageAccess: boolean;
project: ProjectModel;