393 lines
17 KiB
Vue
393 lines
17 KiB
Vue
<script setup lang="ts">
|
|
import { Form, Head } from '@inertiajs/vue3';
|
|
import { computed, ref } from 'vue';
|
|
import ProjectTicketMetadataSettingsController from '@/actions/App/Http/Controllers/ProjectTicketMetadataSettingsController';
|
|
import FeedbackState from '@/components/FeedbackState.vue';
|
|
import InputError from '@/components/InputError.vue';
|
|
import MetadataEditorDialog from '@/components/projects/MetadataEditorDialog.vue';
|
|
import MetadataOptionList from '@/components/projects/MetadataOptionList.vue';
|
|
import TicketDisplaySettings from '@/components/projects/TicketDisplaySettings.vue';
|
|
import TemplateInheritancePanel from '@/components/projects/TemplateInheritancePanel.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 { useLocalization } from '@/composables/useLocalization';
|
|
import AppLayout from '@/layouts/AppLayout.vue';
|
|
import SettingsLayout from '@/layouts/projects/SettingsLayout.vue';
|
|
import { index, show } from '@/routes/projects';
|
|
import { metadata } from '@/routes/projects/settings';
|
|
import type {
|
|
BreadcrumbItem,
|
|
ProjectTicketMetadataSettingsPageProps,
|
|
} from '@/types';
|
|
import type { MetadataEditorTarget } from '@/types/project-metadata';
|
|
|
|
const props = defineProps<ProjectTicketMetadataSettingsPageProps>();
|
|
const { t } = useLocalization();
|
|
const target = ref<MetadataEditorTarget | null>(null);
|
|
const breadcrumbs = computed<BreadcrumbItem[]>(() => [
|
|
{ title: t('navigation.projects'), href: index() },
|
|
{ title: props.project.name, href: show(props.project.id) },
|
|
{ title: t('metadata.title'), href: metadata(props.project.id) },
|
|
]);
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
|
<Head :title="t('metadata.pageTitle', { project: project.name })" />
|
|
<SettingsLayout :project-name="project.name" :project-key="project.key">
|
|
<header class="grid gap-3">
|
|
<h2 class="text-title">{{ t('metadata.title') }}</h2>
|
|
<p class="text-muted-foreground">
|
|
{{ t('metadata.description') }}
|
|
</p>
|
|
</header>
|
|
<FeedbackState
|
|
v-if="status"
|
|
:state="error ? 'error' : 'success'"
|
|
:title="error ? status : t('metadata.updated')"
|
|
/>
|
|
<TemplateInheritancePanel
|
|
:areas="[
|
|
'project_settings',
|
|
'ticket_types',
|
|
'ticket_priorities',
|
|
'ticket_fields',
|
|
'ticket_link_types',
|
|
]"
|
|
:project-id="project.id"
|
|
:template-state="templateState"
|
|
/>
|
|
<TicketDisplaySettings :project="project" />
|
|
<MetadataOptionList
|
|
kind="type"
|
|
:entries="ticketTypes"
|
|
@edit="target = $event"
|
|
/>
|
|
<MetadataOptionList
|
|
kind="priority"
|
|
:entries="ticketPriorities"
|
|
@edit="target = $event"
|
|
/>
|
|
<Card class="gap-0 overflow-hidden">
|
|
<CardHeader class="border-b">
|
|
<CardTitle>Custom fields</CardTitle>
|
|
<CardDescription>
|
|
Define project-specific user or number fields for
|
|
upcoming ticket data.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent class="space-y-4 p-6">
|
|
<Form
|
|
v-bind="
|
|
ProjectTicketMetadataSettingsController.storeFieldDefinition.form(
|
|
project.id,
|
|
)
|
|
"
|
|
class="border-border space-y-4 rounded-xl border border-dashed p-4"
|
|
v-slot="{ errors, processing }"
|
|
>
|
|
<div class="grid gap-2">
|
|
<Label for="field-label">New field label</Label>
|
|
<Input
|
|
id="field-label"
|
|
name="label"
|
|
required
|
|
placeholder="Reviewer"
|
|
/>
|
|
<InputError :message="errors.label" />
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label for="field-type">Field type</Label>
|
|
<select
|
|
id="field-type"
|
|
name="field_type"
|
|
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
|
|
v-for="option in fieldTypeOptions"
|
|
:key="option.value"
|
|
:value="option.value"
|
|
>
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
<InputError :message="errors.field_type" />
|
|
</div>
|
|
|
|
<Button type="submit" :disabled="processing">
|
|
Add field
|
|
</Button>
|
|
</Form>
|
|
|
|
<div
|
|
v-for="fieldDefinition in ticketFieldDefinitions"
|
|
:key="fieldDefinition.id"
|
|
class="border-border rounded-xl border p-4"
|
|
>
|
|
<Form
|
|
v-bind="
|
|
ProjectTicketMetadataSettingsController.updateFieldDefinition.form(
|
|
[project.id, fieldDefinition.id],
|
|
)
|
|
"
|
|
class="grid gap-4"
|
|
v-slot="{ errors, processing }"
|
|
>
|
|
<div class="grid gap-2">
|
|
<Label
|
|
:for="`field-label-${fieldDefinition.id}`"
|
|
>
|
|
Label
|
|
</Label>
|
|
<Input
|
|
:id="`field-label-${fieldDefinition.id}`"
|
|
name="label"
|
|
:default-value="fieldDefinition.label"
|
|
required
|
|
/>
|
|
<InputError :message="errors.label" />
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label
|
|
:for="`field-type-${fieldDefinition.id}`"
|
|
>
|
|
Field type
|
|
</Label>
|
|
<select
|
|
:id="`field-type-${fieldDefinition.id}`"
|
|
name="field_type"
|
|
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
|
|
v-for="option in fieldTypeOptions"
|
|
:key="option.value"
|
|
:value="option.value"
|
|
:selected="
|
|
option.value ===
|
|
fieldDefinition.field_type
|
|
"
|
|
>
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
<InputError :message="errors.field_type" />
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label
|
|
:for="`field-position-${fieldDefinition.id}`"
|
|
>
|
|
Position
|
|
</Label>
|
|
<Input
|
|
:id="`field-position-${fieldDefinition.id}`"
|
|
name="position"
|
|
type="number"
|
|
min="1"
|
|
:max="ticketFieldDefinitions.length"
|
|
:default-value="fieldDefinition.position"
|
|
required
|
|
/>
|
|
<InputError :message="errors.position" />
|
|
</div>
|
|
|
|
<div class="flex flex-wrap justify-between gap-3">
|
|
<Button
|
|
type="submit"
|
|
variant="outline"
|
|
:disabled="processing"
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</Form>
|
|
|
|
<Form
|
|
v-bind="
|
|
ProjectTicketMetadataSettingsController.destroyFieldDefinition.form(
|
|
[project.id, fieldDefinition.id],
|
|
)
|
|
"
|
|
class="mt-3 flex justify-end"
|
|
v-slot="{ processing: deleting }"
|
|
>
|
|
<Button
|
|
type="submit"
|
|
variant="destructive"
|
|
:disabled="deleting"
|
|
>
|
|
Delete
|
|
</Button>
|
|
</Form>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card class="gap-0 overflow-hidden">
|
|
<CardHeader class="border-b">
|
|
<CardTitle>Link types</CardTitle>
|
|
<CardDescription>
|
|
Define project-specific labels for ticket-to-ticket
|
|
relationships.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent class="space-y-4 p-6">
|
|
<Form
|
|
v-bind="
|
|
ProjectTicketMetadataSettingsController.storeLinkType.form(
|
|
project.id,
|
|
)
|
|
"
|
|
class="border-border space-y-4 rounded-xl border border-dashed p-4"
|
|
v-slot="{ errors, processing }"
|
|
>
|
|
<div class="grid gap-2">
|
|
<Label for="link-type-name"> Source label </Label>
|
|
<Input
|
|
id="link-type-name"
|
|
name="name"
|
|
required
|
|
placeholder="Blocks"
|
|
/>
|
|
<InputError :message="errors.name" />
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label for="link-type-counterpart-name">
|
|
Counterpart label
|
|
</Label>
|
|
<Input
|
|
id="link-type-counterpart-name"
|
|
name="counterpart_name"
|
|
required
|
|
placeholder="Blocked by"
|
|
/>
|
|
<InputError :message="errors.counterpart_name" />
|
|
</div>
|
|
|
|
<Button type="submit" :disabled="processing">
|
|
Add link type
|
|
</Button>
|
|
</Form>
|
|
|
|
<div
|
|
v-for="ticketLinkType in ticketLinkTypes"
|
|
:key="ticketLinkType.id"
|
|
class="border-border rounded-xl border p-4"
|
|
>
|
|
<Form
|
|
v-bind="
|
|
ProjectTicketMetadataSettingsController.updateLinkType.form(
|
|
[project.id, ticketLinkType.id],
|
|
)
|
|
"
|
|
class="grid gap-4"
|
|
v-slot="{ errors, processing }"
|
|
>
|
|
<div class="grid gap-2">
|
|
<Label
|
|
:for="`link-type-name-${ticketLinkType.id}`"
|
|
>
|
|
Source label
|
|
</Label>
|
|
<Input
|
|
:id="`link-type-name-${ticketLinkType.id}`"
|
|
name="name"
|
|
:default-value="ticketLinkType.label"
|
|
required
|
|
/>
|
|
<InputError :message="errors.name" />
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label
|
|
:for="`link-type-counterpart-name-${ticketLinkType.id}`"
|
|
>
|
|
Counterpart label
|
|
</Label>
|
|
<Input
|
|
:id="`link-type-counterpart-name-${ticketLinkType.id}`"
|
|
name="counterpart_name"
|
|
:default-value="
|
|
ticketLinkType.counterpart_label
|
|
"
|
|
required
|
|
/>
|
|
<InputError
|
|
:message="errors.counterpart_name"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label
|
|
:for="`link-type-position-${ticketLinkType.id}`"
|
|
>
|
|
Position
|
|
</Label>
|
|
<Input
|
|
:id="`link-type-position-${ticketLinkType.id}`"
|
|
name="position"
|
|
type="number"
|
|
min="1"
|
|
:max="ticketLinkTypes.length"
|
|
:default-value="ticketLinkType.position"
|
|
required
|
|
/>
|
|
<InputError :message="errors.position" />
|
|
</div>
|
|
|
|
<div class="flex flex-wrap justify-between gap-3">
|
|
<Button
|
|
type="submit"
|
|
variant="outline"
|
|
:disabled="processing"
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</Form>
|
|
|
|
<Form
|
|
v-bind="
|
|
ProjectTicketMetadataSettingsController.destroyLinkType.form(
|
|
[project.id, ticketLinkType.id],
|
|
)
|
|
"
|
|
class="mt-3 flex justify-end"
|
|
v-slot="{ processing: deleting }"
|
|
>
|
|
<Button
|
|
type="submit"
|
|
variant="destructive"
|
|
:disabled="deleting"
|
|
>
|
|
Delete
|
|
</Button>
|
|
</Form>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<MetadataEditorDialog
|
|
:project-id="project.id"
|
|
:target="target"
|
|
:types="ticketTypes"
|
|
:priorities="ticketPriorities"
|
|
@close="target = null"
|
|
/>
|
|
</SettingsLayout>
|
|
</AppLayout>
|
|
</template>
|