143 lines
5.6 KiB
Vue
143 lines
5.6 KiB
Vue
<script setup lang="ts">
|
|
import { Braces, Pencil, Plus, Trash2 } from 'lucide-vue-next';
|
|
import { computed, ref } from 'vue';
|
|
import FeedbackState from '@/components/FeedbackState.vue';
|
|
import TicketFieldDefinitionEditorDialog from '@/components/projects/TicketFieldDefinitionEditorDialog.vue';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import type { TicketFieldDefinitionModel } from '@/types/models/ticket-field-definition-model';
|
|
import type { TicketFieldDefinitionEditorTarget } from '@/types/project-metadata';
|
|
import type { TicketFieldTypeOption } from '@/types';
|
|
|
|
const props = defineProps<{
|
|
projectId: number;
|
|
definitions: TicketFieldDefinitionModel[];
|
|
fieldTypeOptions: TicketFieldTypeOption[];
|
|
}>();
|
|
const { t } = useLocalization();
|
|
const target = ref<TicketFieldDefinitionEditorTarget | null>(null);
|
|
const fieldTypeLabels = computed(
|
|
() =>
|
|
new Map(
|
|
props.fieldTypeOptions.map((option) => [
|
|
option.value,
|
|
option.label,
|
|
]),
|
|
),
|
|
);
|
|
function fieldTypeLabel(fieldType: string): string {
|
|
return fieldTypeLabels.value.get(fieldType) ?? fieldType;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
class="bg-card grid min-w-0 gap-4 overflow-hidden rounded-xl border p-4 sm:p-6"
|
|
aria-labelledby="metadata-custom-fields"
|
|
>
|
|
<header class="flex flex-wrap items-center justify-between gap-3">
|
|
<div class="flex min-w-0 items-center gap-2">
|
|
<Braces
|
|
class="text-primary size-5 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<h3 id="metadata-custom-fields" class="font-semibold">
|
|
{{ t('metadata.customFields') }}
|
|
</h3>
|
|
<Badge variant="secondary">{{ definitions.length }}</Badge>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
class="w-full sm:w-auto"
|
|
@click="target = { action: 'create' }"
|
|
>
|
|
<Plus class="size-4" aria-hidden="true" />{{
|
|
t('metadata.addField')
|
|
}}
|
|
</Button>
|
|
</header>
|
|
<p class="text-muted-foreground text-sm">
|
|
{{ t('metadata.customFieldsDescription') }}
|
|
</p>
|
|
<FeedbackState
|
|
v-if="definitions.length === 0"
|
|
state="empty"
|
|
:title="t('metadata.noCustomFields')"
|
|
:description="t('metadata.noCustomFieldsDescription')"
|
|
/>
|
|
<ol v-else class="divide-y" :aria-label="t('metadata.customFields')">
|
|
<li
|
|
v-for="definition in definitions"
|
|
:key="definition.id"
|
|
class="grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 py-4"
|
|
:data-ticket-field-definition="definition.id"
|
|
>
|
|
<span
|
|
class="bg-accent text-accent-foreground flex size-9 shrink-0 items-center justify-center rounded-lg text-sm font-semibold"
|
|
:aria-label="
|
|
t('workflow.positionValue', {
|
|
position: definition.position,
|
|
})
|
|
"
|
|
>{{ definition.position }}</span
|
|
>
|
|
<div class="grid min-w-0 gap-1">
|
|
<p class="break-words font-medium">
|
|
{{ definition.label }}
|
|
</p>
|
|
<div
|
|
class="text-caption text-muted-foreground flex flex-wrap items-center gap-2"
|
|
>
|
|
<Badge variant="outline">{{
|
|
fieldTypeLabel(definition.field_type)
|
|
}}</Badge>
|
|
<span>{{
|
|
t('metadata.storedValues', {
|
|
count:
|
|
definition.ticket_field_values_count ?? 0,
|
|
})
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex shrink-0 gap-1">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
:aria-label="
|
|
t('metadata.editField', { name: definition.label })
|
|
"
|
|
@click="target = { action: 'edit', entry: definition }"
|
|
>
|
|
<Pencil class="size-4" aria-hidden="true" />
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
class="text-destructive"
|
|
:aria-label="
|
|
t('metadata.deleteField', {
|
|
name: definition.label,
|
|
})
|
|
"
|
|
@click="
|
|
target = { action: 'delete', entry: definition }
|
|
"
|
|
>
|
|
<Trash2 class="size-4" aria-hidden="true" />
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
<TicketFieldDefinitionEditorDialog
|
|
:definitions="definitions"
|
|
:field-type-options="fieldTypeOptions"
|
|
:project-id="projectId"
|
|
:target="target"
|
|
@close="target = null"
|
|
/>
|
|
</section>
|
|
</template>
|