Files
KanbanGit/resources/js/pages/projects/settings/Notifications.vue
T
Twyco 3f795ba32e
CI/CD / preconditions (pull_request) Successful in 1s
CI/CD / test_and_check_app (pull_request) Successful in 29m56s
CI/CD / Publish immutable image (pull_request) Skipped
feat(KAN-45): rebuild project notification settings
2026-09-15 22:44:16 +02:00

228 lines
10 KiB
Vue

<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { Bell, Pencil, Plus, ShieldCheck, Trash2 } from 'lucide-vue-next';
import { computed, ref } from 'vue';
import FeedbackState from '@/components/FeedbackState.vue';
import NotificationEditorDialog from '@/components/projects/NotificationEditorDialog.vue';
import TemplateInheritancePanel from '@/components/projects/TemplateInheritancePanel.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { notificationLabel } from '@/lib/notification-labels';
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 { notifications } from '@/routes/projects/settings';
import type {
BreadcrumbItem,
ProjectNotificationSettingsPageProps,
} from '@/types';
import type { NotificationActionModel } from '@/types/models/notification-action-model';
import type { NotificationEditorTarget } from '@/types/project-notifications';
const props = defineProps<ProjectNotificationSettingsPageProps>();
const { t, date: formatDate } = useLocalization();
const target = ref<NotificationEditorTarget | null>(null);
const breadcrumbs = computed<BreadcrumbItem[]>(() => [
{ title: t('navigation.projects'), href: index() },
{ title: props.project.name, href: show(props.project.id) },
{ title: t('notifications.title'), href: notifications(props.project.id) },
]);
function timestamp(entry: NotificationActionModel): string {
const date = entry.updated_at ?? entry.created_at;
if (!date) {
return t('notifications.noChanges');
}
return t(
entry.created_at !== date
? 'notifications.updatedAt'
: 'notifications.createdAt',
{
date: formatDate(new Date(date), {
dateStyle: 'medium',
timeStyle: 'short',
}),
},
);
}
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head
:title="t('notifications.pageTitle', { project: project.name })"
/>
<SettingsLayout :project-name="project.name" :project-key="project.key">
<header class="grid gap-3">
<h2 class="text-title">{{ t('notifications.title') }}</h2>
<p class="text-muted-foreground">
{{ t('notifications.description') }}
</p>
</header>
<FeedbackState
v-if="status"
state="success"
:title="t('notifications.saved')"
/>
<TemplateInheritancePanel
:areas="['notification_actions']"
:project-id="project.id"
:template-state="templateState"
/>
<section
class="grid min-w-0 gap-4"
aria-labelledby="notification-actions-title"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 id="notification-actions-title" class="text-title">
{{ t('notifications.actions') }}
<span class="text-muted-foreground text-sm"
>({{ notificationActions.length }})</span
>
</h3>
<Button @click="target = { action: 'create' }">
<Plus class="size-4" aria-hidden="true" />{{
t('notifications.create')
}}
</Button>
</div>
<div
v-if="notificationActions.length === 0"
class="bg-muted/25 grid justify-items-center gap-3 rounded-xl border border-dashed p-8 text-center"
>
<Bell class="text-primary size-8" aria-hidden="true" />
<h4 class="font-semibold">
{{ t('notifications.empty') }}
</h4>
<p class="text-muted-foreground text-sm">
{{ t('notifications.emptyHelp') }}
</p>
</div>
<ul v-else class="grid min-w-0 gap-3">
<li
v-for="entry in notificationActions"
:key="entry.id"
:data-notification="entry.id"
class="bg-card grid min-w-0 gap-4 rounded-xl border p-4"
>
<div
class="flex flex-wrap items-start justify-between gap-3"
>
<div class="grid min-w-0 flex-1 gap-2">
<h4 class="break-words font-semibold">
{{ entry.name }}
</h4>
<div class="flex flex-wrap gap-2">
<Badge
:variant="
entry.enabled
? 'default'
: 'secondary'
"
>{{
t(
entry.enabled
? 'notifications.enabled'
: 'notifications.disabled',
)
}}</Badge
>
<Badge variant="outline">{{
notificationLabel(
t,
'channel',
entry.channel,
entry.channel_label,
)
}}</Badge>
<Badge
variant="outline"
class="whitespace-normal"
>{{
notificationLabel(
t,
'event',
entry.event_type,
entry.event_type_label,
)
}}</Badge
>
</div>
</div>
<div class="flex gap-1">
<Button
variant="ghost"
size="icon"
:aria-label="
t('notifications.editNamed', {
name: entry.name,
})
"
@click="target = { action: 'edit', entry }"
><Pencil class="size-4" aria-hidden="true"
/></Button>
<Button
variant="ghost"
size="icon"
:aria-label="
t('notifications.deleteNamed', {
name: entry.name,
})
"
@click="
target = { action: 'delete', entry }
"
><Trash2
class="text-destructive size-4"
aria-hidden="true"
/></Button>
</div>
</div>
<dl class="grid min-w-0 gap-3 text-sm sm:grid-cols-2">
<div class="grid gap-1">
<dt class="text-muted-foreground">
{{ t('notifications.delivery') }}
</dt>
<dd class="flex items-center gap-2">
<ShieldCheck
class="size-4 shrink-0"
aria-hidden="true"
/>{{
t(
entry.has_delivery_url
? 'notifications.urlSaved'
: 'notifications.urlMissing',
)
}}
</dd>
</div>
<div class="grid gap-1">
<dt class="text-muted-foreground">
{{ t('notifications.filters') }}
</dt>
<dd>
{{
entry.filters.length
? t('notifications.filterCount', {
count: entry.filters.length,
})
: t('notifications.noFiltersShort')
}}
</dd>
</div>
</dl>
<p class="text-caption text-muted-foreground">
{{ timestamp(entry) }}
</p>
</li>
</ul>
</section>
<NotificationEditorDialog
:target="target"
:settings="props"
@close="target = null"
/>
</SettingsLayout>
</AppLayout>
</template>