350 lines
16 KiB
Vue
350 lines
16 KiB
Vue
<script setup lang="ts">
|
|
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';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
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, ProjectsCreatePageProps } from '@/types';
|
|
|
|
const props = defineProps<ProjectsCreatePageProps>();
|
|
|
|
const breadcrumbs: BreadcrumbItem[] = [
|
|
{
|
|
title: 'Projects',
|
|
href: index(),
|
|
},
|
|
{
|
|
title: 'New project',
|
|
href: create(),
|
|
},
|
|
];
|
|
|
|
const defaultTemplateId =
|
|
props.selectedSourceTemplateId ?? props.sourceTemplates[0]?.id ?? null;
|
|
|
|
const form = useForm({
|
|
copy_areas: [] as string[],
|
|
creation_mode: defaultTemplateId !== null ? 'template' : 'scratch',
|
|
key: '',
|
|
name: '',
|
|
source_project_id: props.sourceProjects[0]?.id
|
|
? String(props.sourceProjects[0].id)
|
|
: '',
|
|
source_template_id:
|
|
defaultTemplateId !== null ? String(defaultTemplateId) : '',
|
|
});
|
|
|
|
const hasSourceProjects = computed(() => props.sourceProjects.length > 0);
|
|
const hasSourceTemplates = computed(() => props.sourceTemplates.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 = [];
|
|
}
|
|
|
|
if (form.creation_mode !== 'template') {
|
|
form.source_template_id = '';
|
|
}
|
|
|
|
form.post(ProjectController.store.url(), {
|
|
preserveScroll: true,
|
|
});
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
|
<Head title="New project" />
|
|
|
|
<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 fresh project, duplicate selected settings from an existing project, or start from a saved template."
|
|
/>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Project details</CardTitle>
|
|
<CardDescription>
|
|
The key is uppercased automatically, must be unique, and
|
|
can contain up to 5 characters.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
<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-3">
|
|
<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>
|
|
|
|
<Label
|
|
class="flex items-start gap-3 rounded-xl border p-4"
|
|
:class="
|
|
hasSourceTemplates
|
|
? 'cursor-pointer'
|
|
: 'cursor-not-allowed opacity-60'
|
|
"
|
|
>
|
|
<input
|
|
v-model="form.creation_mode"
|
|
type="radio"
|
|
value="template"
|
|
class="mt-1"
|
|
:disabled="!hasSourceTemplates"
|
|
/>
|
|
<div class="space-y-1">
|
|
<div class="font-medium">
|
|
Create from template
|
|
</div>
|
|
<p
|
|
class="text-muted-foreground text-sm"
|
|
>
|
|
Reuse a saved configuration
|
|
template.
|
|
</p>
|
|
</div>
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label for="name">Project name</Label>
|
|
<Input
|
|
id="name"
|
|
v-model="form.name"
|
|
required
|
|
autofocus
|
|
placeholder="Kanban foundation"
|
|
/>
|
|
<InputError :message="form.errors.name" />
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label for="key">Project key</Label>
|
|
<Input
|
|
id="key"
|
|
v-model="form.key"
|
|
required
|
|
maxlength="5"
|
|
placeholder="CORE"
|
|
class="uppercase"
|
|
/>
|
|
<p class="text-muted-foreground text-sm">
|
|
Example ticket keys will look like
|
|
<span class="text-foreground font-medium"
|
|
>CORE-1</span
|
|
>.
|
|
</p>
|
|
<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:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 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="form.creation_mode === 'template'"
|
|
class="space-y-4 rounded-xl border border-dashed p-4"
|
|
>
|
|
<div class="grid gap-2">
|
|
<Label for="source_template_id">Template</Label>
|
|
<select
|
|
id="source_template_id"
|
|
v-model="form.source_template_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:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
:disabled="!hasSourceTemplates"
|
|
>
|
|
<option value="">Select a template</option>
|
|
<option
|
|
v-for="sourceTemplate in sourceTemplates"
|
|
:key="sourceTemplate.id"
|
|
:value="String(sourceTemplate.id)"
|
|
>
|
|
{{ sourceTemplate.name }} ({{
|
|
sourceTemplate.slug
|
|
}})
|
|
</option>
|
|
</select>
|
|
<p class="text-muted-foreground text-sm">
|
|
Templates apply their saved configuration
|
|
groups to the new project.
|
|
</p>
|
|
<InputError
|
|
:message="form.errors.source_template_id"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="
|
|
!hasSourceProjects && !hasSourceTemplates
|
|
"
|
|
class="text-muted-foreground text-sm"
|
|
>
|
|
No accessible source projects or saved templates are
|
|
available yet.
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<Button type="submit" :disabled="form.processing">
|
|
Create project
|
|
</Button>
|
|
|
|
<Button as-child variant="outline">
|
|
<Link :href="index()">Cancel</Link>
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</AppLayout>
|
|
</template>
|