210 lines
9.3 KiB
Vue
210 lines
9.3 KiB
Vue
<script setup lang="ts">
|
|
import { Head } from '@inertiajs/vue3';
|
|
import { Columns3, Inbox, Pencil, Plus, Trash2 } from 'lucide-vue-next';
|
|
import { computed, ref } from 'vue';
|
|
import FeedbackState from '@/components/FeedbackState.vue';
|
|
import TemplateInheritancePanel from '@/components/projects/TemplateInheritancePanel.vue';
|
|
import WorkflowEditorDialog from '@/components/projects/WorkflowEditorDialog.vue';
|
|
import WorkflowStatusList from '@/components/projects/WorkflowStatusList.vue';
|
|
import WorkflowTransitionMatrix from '@/components/projects/WorkflowTransitionMatrix.vue';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
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 { board } from '@/routes/projects/settings';
|
|
import type { BreadcrumbItem, ProjectBoardSettingsPageProps } from '@/types';
|
|
import type { WorkflowEditorTarget } from '@/types/project-workflow';
|
|
|
|
const props = defineProps<ProjectBoardSettingsPageProps>();
|
|
const { t } = useLocalization();
|
|
const target = ref<WorkflowEditorTarget | null>(null);
|
|
const breadcrumbs = computed<BreadcrumbItem[]>(() => [
|
|
{ title: t('navigation.projects'), href: index() },
|
|
{ title: props.project.name, href: show(props.project.id) },
|
|
{ title: t('workflow.title'), href: board(props.project.id) },
|
|
]);
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
|
<Head :title="t('workflow.pageTitle', { project: project.name })" />
|
|
<SettingsLayout :project-name="project.name" :project-key="project.key">
|
|
<header class="grid gap-3">
|
|
<h2 class="text-title">{{ t('workflow.title') }}</h2>
|
|
<p class="text-muted-foreground">
|
|
{{ t('workflow.description') }}
|
|
</p>
|
|
</header>
|
|
<FeedbackState
|
|
v-if="status"
|
|
state="success"
|
|
:title="t('workflow.updated')"
|
|
/>
|
|
<TemplateInheritancePanel
|
|
:project-id="project.id"
|
|
:template-state="templateState"
|
|
:areas="[
|
|
'board_columns',
|
|
'ticket_statuses',
|
|
'status_transitions',
|
|
]"
|
|
/>
|
|
<Card
|
|
class="min-w-0 gap-0 overflow-hidden rounded-xl py-0 shadow-none"
|
|
>
|
|
<CardHeader class="gap-stack grid-cols-1 border-b p-5 sm:p-6">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<Inbox
|
|
class="text-primary size-5 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<h3 class="font-semibold">
|
|
{{ t('workflow.backlog') }}
|
|
</h3>
|
|
<Badge variant="secondary">{{
|
|
t('workflow.offBoard')
|
|
}}</Badge>
|
|
</div>
|
|
<p class="text-muted-foreground text-sm">
|
|
{{ t('workflow.backlogDescription') }}
|
|
</p>
|
|
</CardHeader>
|
|
<CardContent class="p-5 sm:p-6"
|
|
><WorkflowStatusList
|
|
:column="null"
|
|
:statuses="backlogTicketStatuses"
|
|
@edit="target = $event"
|
|
/></CardContent>
|
|
</Card>
|
|
<section
|
|
class="grid min-w-0 gap-4"
|
|
aria-labelledby="workflow-columns"
|
|
>
|
|
<header
|
|
class="flex flex-wrap items-center justify-between gap-3"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<Columns3
|
|
class="text-primary size-5"
|
|
aria-hidden="true"
|
|
/>
|
|
<h3 id="workflow-columns" class="font-semibold">
|
|
{{ t('workflow.columns') }}
|
|
</h3>
|
|
<Badge variant="secondary">{{
|
|
boardColumns.length
|
|
}}</Badge>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
class="w-full sm:w-auto"
|
|
@click="target = { kind: 'column', action: 'create' }"
|
|
><Plus class="size-4" aria-hidden="true" />{{
|
|
t('workflow.addColumn')
|
|
}}</Button
|
|
>
|
|
</header>
|
|
<p class="text-muted-foreground text-sm">
|
|
{{ t('workflow.columnsDescription') }}
|
|
</p>
|
|
<FeedbackState
|
|
v-if="boardColumns.length === 0"
|
|
state="empty"
|
|
:title="t('workflow.emptyColumns')"
|
|
:description="t('workflow.emptyColumnsDescription')"
|
|
/>
|
|
<ol
|
|
v-else
|
|
class="grid min-w-0 gap-4"
|
|
:aria-label="t('workflow.columns')"
|
|
>
|
|
<li
|
|
v-for="column in boardColumns"
|
|
:key="column.id"
|
|
class="bg-card min-w-0 overflow-hidden rounded-xl border"
|
|
:data-column-id="column.id"
|
|
>
|
|
<header
|
|
class="bg-muted/35 flex min-w-0 flex-wrap items-center gap-3 border-b p-4 sm:p-5"
|
|
>
|
|
<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: column.position,
|
|
})
|
|
"
|
|
>{{ column.position }}</span
|
|
>
|
|
<h4
|
|
class="min-w-0 flex-1 basis-24 break-words font-semibold"
|
|
>
|
|
{{ column.name }}
|
|
</h4>
|
|
<div class="flex gap-1">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
:aria-label="
|
|
t('workflow.changeColumn', {
|
|
name: column.name,
|
|
})
|
|
"
|
|
@click="
|
|
target = {
|
|
kind: 'column',
|
|
action: 'edit',
|
|
column,
|
|
}
|
|
"
|
|
><Pencil class="size-4" aria-hidden="true"
|
|
/></Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
class="text-destructive"
|
|
:aria-label="
|
|
t('workflow.removeColumn', {
|
|
name: column.name,
|
|
})
|
|
"
|
|
@click="
|
|
target = {
|
|
kind: 'column',
|
|
action: 'delete',
|
|
column,
|
|
}
|
|
"
|
|
><Trash2 class="size-4" aria-hidden="true"
|
|
/></Button>
|
|
</div>
|
|
</header>
|
|
<div class="p-4 sm:p-5">
|
|
<WorkflowStatusList
|
|
:column="column"
|
|
:statuses="column.ticket_statuses ?? []"
|
|
@edit="target = $event"
|
|
/>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
<WorkflowTransitionMatrix
|
|
:project-id="project.id"
|
|
:statuses="ticketStatuses"
|
|
/>
|
|
<WorkflowEditorDialog
|
|
:project-id="project.id"
|
|
:column-count="boardColumns.length"
|
|
:target="target"
|
|
@close="target = null"
|
|
/>
|
|
</SettingsLayout>
|
|
</AppLayout>
|
|
</template>
|