178 lines
6.1 KiB
Vue
178 lines
6.1 KiB
Vue
<script setup lang="ts">
|
|
import { useForm } from '@inertiajs/vue3';
|
|
import ProjectController from '@/actions/App/Http/Controllers/ProjectController';
|
|
import InputError from '@/components/InputError.vue';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { Label } from '@/components/ui/label';
|
|
import type { ProjectTemplateState, ProjectTemplateStateArea } from '@/types';
|
|
import { computed } from 'vue';
|
|
|
|
const props = defineProps<{
|
|
areas: string[];
|
|
projectId: number;
|
|
templateState?: ProjectTemplateState | null;
|
|
}>();
|
|
|
|
const form = useForm<{
|
|
areas: string[];
|
|
status_replacements: Record<string, number | undefined>;
|
|
}>({
|
|
areas: [],
|
|
status_replacements: {},
|
|
});
|
|
|
|
const activeArea = computed(() => form.areas[0]);
|
|
|
|
const inheritedAreas = computed(
|
|
() =>
|
|
props.templateState?.inheritedAreas.filter((area) =>
|
|
props.areas.includes(area.value),
|
|
) ?? [],
|
|
);
|
|
|
|
const overriddenAreas = computed(
|
|
() =>
|
|
props.templateState?.overriddenAreas.filter((area) =>
|
|
props.areas.includes(area.value),
|
|
) ?? [],
|
|
);
|
|
|
|
const statusRequirement = computed(
|
|
() => props.templateState?.resetRequirements?.ticket_statuses,
|
|
);
|
|
|
|
const canReset = (area: ProjectTemplateStateArea): boolean => {
|
|
if (area.value !== 'ticket_statuses') {
|
|
return true;
|
|
}
|
|
|
|
return (
|
|
statusRequirement.value?.removedStatuses.every(
|
|
(status) =>
|
|
form.status_replacements[String(status.id)] !== undefined,
|
|
) ?? true
|
|
);
|
|
};
|
|
|
|
const resetArea = (area: ProjectTemplateStateArea): void => {
|
|
if (!canReset(area)) {
|
|
return;
|
|
}
|
|
|
|
form.areas = [area.value];
|
|
form.post(ProjectController.resetTemplate.url(props.projectId), {
|
|
preserveScroll: true,
|
|
onSuccess: () => form.reset(),
|
|
});
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Card
|
|
v-if="
|
|
templateState?.inheritsFromTemplate &&
|
|
(inheritedAreas.length > 0 || overriddenAreas.length > 0)
|
|
"
|
|
>
|
|
<CardHeader>
|
|
<CardTitle>
|
|
Template inheritance: {{ templateState.templateName }}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Every setting area inherits independently. Local changes stay
|
|
unchanged when the template is updated until that area is reset.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent class="space-y-4">
|
|
<div class="flex flex-wrap gap-2">
|
|
<Badge
|
|
v-for="area in inheritedAreas"
|
|
:key="`inherited-${area.value}`"
|
|
variant="outline"
|
|
>
|
|
{{ area.label }} · inherited
|
|
</Badge>
|
|
</div>
|
|
|
|
<div
|
|
v-for="area in overriddenAreas"
|
|
:key="`overridden-${area.value}`"
|
|
class="space-y-3 rounded-lg border p-4"
|
|
>
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<Badge>{{ area.label }} · template overridden</Badge>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
:disabled="form.processing || !canReset(area)"
|
|
@click="resetArea(area)"
|
|
>
|
|
{{
|
|
activeArea === area.value && form.processing
|
|
? 'Resetting…'
|
|
: 'Reset to template'
|
|
}}
|
|
</Button>
|
|
</div>
|
|
|
|
<div
|
|
v-if="area.value === 'ticket_statuses' && statusRequirement"
|
|
class="space-y-3"
|
|
>
|
|
<p class="text-muted-foreground text-sm">
|
|
Choose where tickets in local statuses should move
|
|
before those statuses are removed.
|
|
</p>
|
|
<div
|
|
v-for="status in statusRequirement.removedStatuses"
|
|
:key="status.id"
|
|
class="grid gap-2 sm:grid-cols-2 sm:items-center"
|
|
>
|
|
<Label :for="`replacement-${status.id}`">
|
|
{{ status.name }} ({{ status.ticketCount }} tickets)
|
|
</Label>
|
|
<div class="grid gap-1">
|
|
<select
|
|
:id="`replacement-${status.id}`"
|
|
v-model="
|
|
form.status_replacements[String(status.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 focus-visible:ring-[3px]"
|
|
>
|
|
<option :value="undefined" disabled>
|
|
Select replacement status
|
|
</option>
|
|
<option
|
|
v-for="option in statusRequirement.replacementOptions"
|
|
:key="option.id"
|
|
:value="option.id"
|
|
>
|
|
{{ option.name }}
|
|
</option>
|
|
</select>
|
|
<InputError
|
|
:message="
|
|
form.errors[
|
|
`status_replacements.${status.id}`
|
|
]
|
|
"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<InputError :message="form.errors.areas" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</template>
|