fix(KAN-14): preserve template inheritance and project data
CI/CD / preconditions (pull_request) Successful in 4s
CI/CD / test_and_check_app (pull_request) Failing after 2m14s
CI/CD / Publish immutable image (pull_request) Skipped

This commit is contained in:
2026-10-15 11:38:17 +02:00
parent 5769d84526
commit 1291a0fcad
61 changed files with 2696 additions and 335 deletions
@@ -3,7 +3,6 @@ import { Link, useForm } from '@inertiajs/vue3';
import { Layers3, LockKeyhole } from 'lucide-vue-next';
import { computed } from 'vue';
import FormFeedback from '@/components/FormFeedback.vue';
import FeedbackState from '@/components/FeedbackState.vue';
import InputError from '@/components/InputError.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -12,7 +11,13 @@ import { Label } from '@/components/ui/label';
import { useFormProtection } from '@/composables/useFormProtection';
import { useLocalization } from '@/composables/useLocalization';
import { useProjectTemplateAreas } from '@/composables/useProjectTemplateAreas';
import { index, update } from '@/routes/project-templates';
import {
index,
update,
store as storeStandalone,
} from '@/routes/project-templates';
import TemplateConfigurationEditor from '@/components/projects/TemplateConfigurationEditor.vue';
import { createTemplateConfiguration } from '@/lib/project-template-configuration';
import { show } from '@/routes/projects';
import { store } from '@/routes/projects/templates';
import type { ProjectDuplicateCopyAreaOption } from '@/types';
@@ -22,11 +27,18 @@ import type { ProjectTemplateModel } from '@/types/models/project-template-model
const props = defineProps<{
copyAreaOptions: ProjectDuplicateCopyAreaOption[];
project?: ProjectModel;
project?: ProjectModel | null;
initialConfiguration?: Record<string, unknown>;
template?: ProjectTemplateModel;
}>();
const { t } = useLocalization();
const { areaText } = useProjectTemplateAreas();
const publishingBase = computed(() =>
props.project?.project_template &&
!props.project.project_template.is_published
? props.project.project_template
: null,
);
const source = computed(() => props.template?.source_project ?? props.project);
const linkedCount = computed(() => props.template?.linked_project_count ?? 0);
const form = useForm(
@@ -34,14 +46,23 @@ const form = useForm(
{
name:
props.template?.name ??
t('templates.defaultName', { project: props.project?.name ?? '' }),
(props.project
? t('templates.defaultName', { project: props.project.name })
: ''),
slug:
props.template?.slug ??
`${props.project?.key.toLowerCase()}-template`,
publishingBase.value?.slug ??
(props.project
? `${props.project.key.toLowerCase()}-template`
: ''),
copy_areas: props.template
? [...props.template.copy_areas]
: props.copyAreaOptions.map((area) => area.value),
confirm_linked_projects_update: false,
configuration_snapshot: createTemplateConfiguration(
props.template?.configuration_snapshot ??
props.initialConfiguration,
),
},
);
const protection = useFormProtection(form);
@@ -53,13 +74,10 @@ const copyAreasError = computed(
)?.[1],
);
const cancelTarget = computed(() =>
props.template ? index() : show(props.project!.id),
props.project ? show(props.project.id) : index(),
);
function submit(): void {
if (
!source.value ||
(linkedCount.value > 0 && !form.confirm_linked_projects_update)
) {
if (linkedCount.value > 0 && !form.confirm_linked_projects_update) {
return;
}
form.transform((data) =>
@@ -67,6 +85,7 @@ function submit(): void {
? {
name: data.name,
copy_areas: data.copy_areas,
configuration_snapshot: data.configuration_snapshot,
...(linkedCount.value > 0
? {
confirm_linked_projects_update:
@@ -74,10 +93,21 @@ function submit(): void {
}
: {}),
}
: { name: data.name, slug: data.slug, copy_areas: data.copy_areas },
: {
name: data.name,
slug: data.slug,
copy_areas: data.copy_areas,
...(!props.project
? { configuration_snapshot: data.configuration_snapshot }
: {}),
},
);
protection.submit(
props.template ? update(props.template.id) : store(props.project!.id),
props.template
? update(props.template.id)
: props.project
? store(props.project.id)
: storeStandalone(),
{ preserveScroll: true },
);
}
@@ -90,12 +120,6 @@ function submit(): void {
@submit.prevent="submit"
>
<FormFeedback :state="protection.state.value" @retry="submit" />
<FeedbackState
v-if="!source"
state="error"
:title="t('templates.missingSource')"
:description="t('templates.missingSourceHelp')"
/>
<div
class="gap-page grid min-w-0 items-start lg:grid-cols-[minmax(0,1fr)_18rem]"
>
@@ -115,7 +139,7 @@ function submit(): void {
name="name"
required
maxlength="255"
:disabled="form.processing || !source"
:disabled="form.processing"
:placeholder="t('templates.namePlaceholder')"
:invalid="!!form.errors.name"
aria-describedby="name-error"
@@ -139,7 +163,11 @@ function submit(): void {
name="slug"
required
maxlength="255"
:disabled="!!template || form.processing || !source"
:disabled="
!!template ||
!!publishingBase ||
form.processing
"
:placeholder="t('templates.slugPlaceholder')"
:invalid="!!form.errors.slug"
aria-describedby="slug-help slug-error"
@@ -152,7 +180,7 @@ function submit(): void {
>
{{
t(
template
template || publishingBase
? 'templates.slugLocked'
: 'templates.slugHelp',
)
@@ -166,7 +194,7 @@ function submit(): void {
</section>
<fieldset
class="bg-card min-w-0 rounded-md border p-5"
:disabled="form.processing || !source"
:disabled="form.processing"
aria-describedby="areas-help copy-areas-error"
>
<legend class="text-title px-1">
@@ -231,6 +259,14 @@ function submit(): void {
:message="copyAreasError"
/>
</fieldset>
<TemplateConfigurationEditor
v-if="template || !project"
v-model="form.configuration_snapshot"
:areas="form.copy_areas"
:options="copyAreaOptions"
:errors="form.errors"
:disabled="form.processing"
/>
</div>
<aside
class="bg-card gap-column grid min-w-0 rounded-md border p-5"
@@ -310,7 +346,7 @@ function submit(): void {
id="confirm-linked"
v-model="form.confirm_linked_projects_update"
type="checkbox"
:disabled="form.processing || !source"
:disabled="form.processing"
:aria-invalid="!!form.errors.confirm_linked_projects_update"
aria-describedby="linked-help linked-error"
class="accent-primary mt-0.5 size-5 shrink-0"
@@ -335,8 +371,7 @@ function submit(): void {
:loading="form.processing"
:loading-label="t('forms.saving')"
:disabled="
!source ||
(linkedCount > 0 && !form.confirm_linked_projects_update)
linkedCount > 0 && !form.confirm_linked_projects_update
"
>{{
t(template ? 'templates.saveChanges' : 'templates.saveNew')