replaced allowed mentions with enum
linter / quality (pull_request) Successful in 1m20s
tests / ci (8.4) (pull_request) Failing after 1m31s

This commit is contained in:
2026-04-25 18:55:05 +02:00
parent 55db0ac642
commit e66b798429
9 changed files with 131 additions and 189 deletions
@@ -2,6 +2,7 @@
namespace App\Actions\Notifications; namespace App\Actions\Notifications;
use App\Enums\DiscordAllowedMentionType;
use App\Enums\NotificationChannel; use App\Enums\NotificationChannel;
use App\Enums\NotificationEventType; use App\Enums\NotificationEventType;
use App\Http\Resources\NotificationActionResource; use App\Http\Resources\NotificationActionResource;
@@ -22,6 +23,7 @@ class BuildProjectNotificationSettingsPayload
return [ return [
'channelOptions' => NotificationChannel::options(), 'channelOptions' => NotificationChannel::options(),
'eventTypeOptions' => NotificationEventType::options(), 'eventTypeOptions' => NotificationEventType::options(),
'discordAllowedMentionOptions' => DiscordAllowedMentionType::options(),
'notificationActions' => NotificationActionResource::collection( 'notificationActions' => NotificationActionResource::collection(
$this->listProjectNotificationActions->handle($project), $this->listProjectNotificationActions->handle($project),
)->resolve(), )->resolve(),
@@ -2,6 +2,7 @@
namespace App\Actions\Notifications; namespace App\Actions\Notifications;
use App\Enums\DiscordAllowedMentionType;
use App\Enums\NotificationChannel; use App\Enums\NotificationChannel;
use App\Enums\NotificationDeliveryStatus; use App\Enums\NotificationDeliveryStatus;
use App\Models\NotificationAction; use App\Models\NotificationAction;
@@ -21,11 +22,6 @@ class DeliverNotificationAction implements ShouldQueue
private const int DISCORD_CONTENT_LIMIT = 2000; private const int DISCORD_CONTENT_LIMIT = 2000;
/**
* @var list<string>
*/
private const array DISCORD_ALLOWED_MENTION_TYPES = ['everyone', 'roles', 'users'];
public int $tries = 3; public int $tries = 3;
public int $timeout = 15; public int $timeout = 15;
@@ -318,7 +314,7 @@ class DeliverNotificationAction implements ShouldQueue
continue; continue;
} }
if (! in_array($allowedMention, self::DISCORD_ALLOWED_MENTION_TYPES, true)) { if (! in_array($allowedMention, DiscordAllowedMentionType::values(), true)) {
continue; continue;
} }
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Enums;
enum DiscordAllowedMentionType: string
{
case Everyone = 'everyone';
case Roles = 'roles';
case Users = 'users';
public function label(): string
{
return match ($this) {
self::Everyone => '@everyone and @here',
self::Roles => 'Role mentions (@role)',
self::Users => 'User mentions (@username)',
};
}
/**
* @return array<int, array{label: string, value: string}>
*/
public static function options(): array
{
return array_map(
fn (self $eventType): array => [
'label' => $eventType->label(),
'value' => $eventType->value,
],
self::cases(),
);
}
/**
* @param array<int, self>|null $discordAllowedMentionTypes
* @return array<int, string>
*/
public static function values(?array $discordAllowedMentionTypes = null): array
{
return array_map(
fn (self $discordAllowedMentionType): string => $discordAllowedMentionType->value,
$discordAllowedMentionTypes ?? self::cases(),
);
}
}
+12
View File
@@ -28,4 +28,16 @@ enum NotificationChannel: string
self::cases(), self::cases(),
); );
} }
/**
* @param array<int, self>|null $notificationChannels
* @return array<int, string>
*/
public static function values(?array $notificationChannels = null): array
{
return array_map(
fn (self $notificationChannel): string => $notificationChannel->value,
$notificationChannels ?? self::cases(),
);
}
} }
+12
View File
@@ -26,4 +26,16 @@ enum NotificationEventType: string
self::cases(), self::cases(),
); );
} }
/**
* @param array<int, self>|null $notificationEventTypes
* @return array<int, string>
*/
public static function values(?array $notificationEventTypes = null): array
{
return array_map(
fn (self $notificationEventType): string => $notificationEventType->value,
$notificationEventTypes ?? self::cases(),
);
}
} }
@@ -2,6 +2,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Enums\DiscordAllowedMentionType;
use App\Enums\NotificationChannel; use App\Enums\NotificationChannel;
use App\Enums\NotificationEventType; use App\Enums\NotificationEventType;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
@@ -10,11 +11,6 @@ use Illuminate\Validation\Rule;
class StoreProjectNotificationActionRequest extends FormRequest class StoreProjectNotificationActionRequest extends FormRequest
{ {
/**
* @var list<string>
*/
private const array DISCORD_ALLOWED_MENTION_TYPES = ['everyone', 'roles', 'users'];
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
*/ */
@@ -32,60 +28,14 @@ class StoreProjectNotificationActionRequest extends FormRequest
{ {
return [ return [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'event_type' => ['required', 'string', Rule::in($this->eventTypes())], 'event_type' => ['required', 'string', Rule::in(NotificationEventType::values())],
'channel' => ['required', 'string', Rule::in($this->channels())], 'channel' => ['required', 'string', Rule::in(NotificationChannel::values())],
'channel_configuration' => ['nullable', 'array'], 'channel_configuration' => ['nullable', 'array'],
'channel_configuration.allowed_mentions' => ['nullable', 'array'], 'channel_configuration.allowed_mentions' => ['nullable', 'array'],
'channel_configuration.allowed_mentions.*' => $this->allowedMentionRules(), 'channel_configuration.allowed_mentions.*' => ['nullable', 'string', Rule::in(DiscordAllowedMentionType::values())],
'enabled' => ['required', 'boolean'], 'enabled' => ['required', 'boolean'],
'delivery_url' => ['required', 'string', 'max:2048', 'url'], 'delivery_url' => ['required', 'string', 'max:2048', 'url'],
'message_template' => $this->messageTemplateRules(), 'message_template' => [Rule::requiredIf($this->input('channel') === NotificationChannel::Discord->value), 'string', 'max:2000'],
];
}
/**
* @return array<int, string>
*/
private function channels(): array
{
return array_map(
static fn (NotificationChannel $channel): string => $channel->value,
NotificationChannel::cases(),
);
}
/**
* @return array<int, string>
*/
private function eventTypes(): array
{
return array_map(
static fn (NotificationEventType $eventType): string => $eventType->value,
NotificationEventType::cases(),
);
}
/**
* @return array<int, ValidationRule|string>
*/
private function messageTemplateRules(): array
{
if ($this->input('channel') === NotificationChannel::Discord->value) {
return ['required', 'string', 'max:2000'];
}
return ['nullable', 'string', 'max:2000'];
}
/**
* @return array<int, ValidationRule|string>
*/
private function allowedMentionRules(): array
{
return [
'nullable',
'string',
Rule::in(self::DISCORD_ALLOWED_MENTION_TYPES),
]; ];
} }
} }
@@ -2,6 +2,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Enums\DiscordAllowedMentionType;
use App\Enums\NotificationChannel; use App\Enums\NotificationChannel;
use App\Enums\NotificationEventType; use App\Enums\NotificationEventType;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
@@ -10,10 +11,6 @@ use Illuminate\Validation\Rule;
class UpdateProjectNotificationActionRequest extends FormRequest class UpdateProjectNotificationActionRequest extends FormRequest
{ {
/**
* @var list<string>
*/
private const array DISCORD_ALLOWED_MENTION_TYPES = ['everyone', 'roles', 'users'];
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
@@ -32,60 +29,14 @@ class UpdateProjectNotificationActionRequest extends FormRequest
{ {
return [ return [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'event_type' => ['required', 'string', Rule::in($this->eventTypes())], 'event_type' => ['required', 'string', Rule::in(NotificationEventType::values())],
'channel' => ['required', 'string', Rule::in($this->channels())], 'channel' => ['required', 'string', Rule::in(NotificationChannel::values())],
'channel_configuration' => ['nullable', 'array'], 'channel_configuration' => ['nullable', 'array'],
'channel_configuration.allowed_mentions' => ['nullable', 'array'], 'channel_configuration.allowed_mentions' => ['nullable', 'array'],
'channel_configuration.allowed_mentions.*' => $this->allowedMentionRules(), 'channel_configuration.allowed_mentions.*' => ['nullable', 'string', Rule::in(DiscordAllowedMentionType::values())],
'enabled' => ['required', 'boolean'], 'enabled' => ['required', 'boolean'],
'delivery_url' => ['nullable', 'string', 'max:2048', 'url'], 'delivery_url' => ['nullable', 'string', 'max:2048', 'url'],
'message_template' => $this->messageTemplateRules(), 'message_template' => [Rule::requiredIf($this->input('channel') === NotificationChannel::Discord->value), 'string', 'max:2000'],
];
}
/**
* @return array<int, string>
*/
private function channels(): array
{
return array_map(
static fn (NotificationChannel $channel): string => $channel->value,
NotificationChannel::cases(),
);
}
/**
* @return array<int, string>
*/
private function eventTypes(): array
{
return array_map(
static fn (NotificationEventType $eventType): string => $eventType->value,
NotificationEventType::cases(),
);
}
/**
* @return array<int, ValidationRule|string>
*/
private function messageTemplateRules(): array
{
if ($this->input('channel') === NotificationChannel::Discord->value) {
return ['required', 'string', 'max:2000'];
}
return ['nullable', 'string', 'max:2000'];
}
/**
* @return array<int, ValidationRule|string>
*/
private function allowedMentionRules(): array
{
return [
'nullable',
'string',
Rule::in(self::DISCORD_ALLOWED_MENTION_TYPES),
]; ];
} }
} }
@@ -44,23 +44,6 @@ const timestampFormatter = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium', dateStyle: 'medium',
timeStyle: 'short', timeStyle: 'short',
}); });
const discordAllowedMentionOptions: Array<{
value: string;
label: string;
}> = [
{
value: 'users',
label: 'User mentions (@username)',
},
{
value: 'roles',
label: 'Role mentions (@role)',
},
{
value: 'everyone',
label: '@everyone and @here',
},
];
const breadcrumbs: BreadcrumbItem[] = [ const breadcrumbs: BreadcrumbItem[] = [
{ {
@@ -203,8 +186,8 @@ const notificationActionTimestamp = (
<CardHeader> <CardHeader>
<CardTitle>Create notification</CardTitle> <CardTitle>Create notification</CardTitle>
<CardDescription> <CardDescription>
Add a project-scoped notification action for Add a project-scoped notification action for the
the supported ticket status change event. supported ticket status change event.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@@ -262,7 +245,9 @@ const notificationActionTimestamp = (
</div> </div>
<div class="grid gap-2"> <div class="grid gap-2">
<Label for="create-notification-channel"> <Label
for="create-notification-channel"
>
Channel Channel
</Label> </Label>
<select <select
@@ -284,7 +269,9 @@ const notificationActionTimestamp = (
</div> </div>
<div class="grid gap-2"> <div class="grid gap-2">
<Label for="create-notification-delivery-url"> <Label
for="create-notification-delivery-url"
>
Delivery URL Delivery URL
</Label> </Label>
<Input <Input
@@ -298,7 +285,9 @@ const notificationActionTimestamp = (
never rendered back to the browser after never rendered back to the browser after
save. save.
</p> </p>
<InputError :message="errors.delivery_url" /> <InputError
:message="errors.delivery_url"
/>
</div> </div>
<div class="grid gap-2"> <div class="grid gap-2">
@@ -491,7 +480,11 @@ const notificationActionTimestamp = (
</Badge> </Badge>
</div> </div>
<CardDescription> <CardDescription>
{{ notificationActionTimestamp(notificationAction) }} {{
notificationActionTimestamp(
notificationAction,
)
}}
</CardDescription> </CardDescription>
</div> </div>
</CardHeader> </CardHeader>
@@ -503,25 +496,19 @@ const notificationActionTimestamp = (
[project.id, notificationAction.id], [project.id, notificationAction.id],
) )
" "
:error-bag=" :error-bag="`updateNotificationAction-${notificationAction.id}`"
`updateNotificationAction-${notificationAction.id}`
"
class="space-y-4" class="space-y-4"
v-slot="{ errors, processing }" v-slot="{ errors, processing }"
> >
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
<div class="grid gap-2"> <div class="grid gap-2">
<Label <Label
:for=" :for="`notification-name-${notificationAction.id}`"
`notification-name-${notificationAction.id}`
"
> >
Name Name
</Label> </Label>
<Input <Input
:id=" :id="`notification-name-${notificationAction.id}`"
`notification-name-${notificationAction.id}`
"
name="name" name="name"
:default-value=" :default-value="
notificationAction.name notificationAction.name
@@ -532,16 +519,12 @@ const notificationActionTimestamp = (
<div class="grid gap-2"> <div class="grid gap-2">
<Label <Label
:for=" :for="`notification-delivery-url-${notificationAction.id}`"
`notification-delivery-url-${notificationAction.id}`
"
> >
Delivery URL Delivery URL
</Label> </Label>
<Input <Input
:id=" :id="`notification-delivery-url-${notificationAction.id}`"
`notification-delivery-url-${notificationAction.id}`
"
name="delivery_url" name="delivery_url"
type="url" type="url"
:placeholder=" :placeholder="
@@ -566,16 +549,12 @@ const notificationActionTimestamp = (
<div class="grid gap-2"> <div class="grid gap-2">
<Label <Label
:for=" :for="`notification-event-type-${notificationAction.id}`"
`notification-event-type-${notificationAction.id}`
"
> >
Event type Event type
</Label> </Label>
<select <select
:id=" :id="`notification-event-type-${notificationAction.id}`"
`notification-event-type-${notificationAction.id}`
"
name="event_type" name="event_type"
:default-value=" :default-value="
notificationAction.event_type notificationAction.event_type
@@ -597,16 +576,12 @@ const notificationActionTimestamp = (
<div class="grid gap-2"> <div class="grid gap-2">
<Label <Label
:for=" :for="`notification-channel-${notificationAction.id}`"
`notification-channel-${notificationAction.id}`
"
> >
Channel Channel
</Label> </Label>
<select <select
:id=" :id="`notification-channel-${notificationAction.id}`"
`notification-channel-${notificationAction.id}`
"
name="channel" name="channel"
v-model=" v-model="
editChannelState[ editChannelState[
@@ -632,8 +607,9 @@ const notificationActionTimestamp = (
<div <div
:class="[ :class="[
'grid gap-2', 'grid gap-2',
selectedChannel(notificationAction) !== selectedChannel(
'discord' notificationAction,
) !== 'discord'
? 'hidden' ? 'hidden'
: '', : '',
]" ]"
@@ -657,8 +633,9 @@ const notificationActionTimestamp = (
) )
" "
:disabled=" :disabled="
selectedChannel(notificationAction) !== selectedChannel(
'discord' notificationAction,
) !== 'discord'
" "
class="border-input text-primary focus:ring-ring size-4 rounded" class="border-input text-primary focus:ring-ring size-4 rounded"
/> />
@@ -669,8 +646,9 @@ const notificationActionTimestamp = (
</div> </div>
<p class="text-muted-foreground text-sm"> <p class="text-muted-foreground text-sm">
{{ {{
selectedChannel(notificationAction) === selectedChannel(
'discord' notificationAction,
) === 'discord'
? 'Pick which Discord mention types are allowed. Leave all unchecked to keep mentions fully disabled.' ? 'Pick which Discord mention types are allowed. Leave all unchecked to keep mentions fully disabled.'
: 'Allowed mentions are only used when the channel is Discord.' : 'Allowed mentions are only used when the channel is Discord.'
}} }}
@@ -682,16 +660,12 @@ const notificationActionTimestamp = (
<div class="grid gap-2"> <div class="grid gap-2">
<Label <Label
:for=" :for="`notification-message-template-${notificationAction.id}`"
`notification-message-template-${notificationAction.id}`
"
> >
Discord message template Discord message template
</Label> </Label>
<textarea <textarea
:id=" :id="`notification-message-template-${notificationAction.id}`"
`notification-message-template-${notificationAction.id}`
"
name="message_template" name="message_template"
rows="4" rows="4"
:value=" :value="
@@ -699,13 +673,15 @@ const notificationActionTimestamp = (
'' ''
" "
:disabled=" :disabled="
selectedChannel(notificationAction) !== selectedChannel(
'discord' notificationAction,
) !== 'discord'
" "
:class="[ :class="[
'border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 min-h-28 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]', 'border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 min-h-28 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]',
selectedChannel(notificationAction) !== selectedChannel(
'discord' notificationAction,
) !== 'discord'
? 'hidden' ? 'hidden'
: 'block', : 'block',
]" ]"
@@ -713,8 +689,9 @@ const notificationActionTimestamp = (
/> />
<p class="text-muted-foreground text-sm"> <p class="text-muted-foreground text-sm">
{{ {{
selectedChannel(notificationAction) === selectedChannel(
'discord' notificationAction,
) === 'discord'
? 'Available for Discord actions only. Use the fixed variables listed in the sidebar.' ? 'Available for Discord actions only. Use the fixed variables listed in the sidebar.'
: 'Discord message templates are only used when the channel is Discord.' : 'Discord message templates are only used when the channel is Discord.'
}} }}
@@ -729,9 +706,7 @@ const notificationActionTimestamp = (
> >
<div class="grid gap-2"> <div class="grid gap-2">
<Label <Label
:for=" :for="`notification-enabled-${notificationAction.id}`"
`notification-enabled-${notificationAction.id}`
"
class="flex items-center gap-3" class="flex items-center gap-3"
> >
<input <input
@@ -740,9 +715,7 @@ const notificationActionTimestamp = (
value="0" value="0"
/> />
<input <input
:id=" :id="`notification-enabled-${notificationAction.id}`"
`notification-enabled-${notificationAction.id}`
"
type="checkbox" type="checkbox"
name="enabled" name="enabled"
value="1" value="1"
+1
View File
@@ -109,6 +109,7 @@ export type ProjectRepositorySettingsPageProps = {
export type ProjectNotificationSettingsPageProps = { export type ProjectNotificationSettingsPageProps = {
channelOptions: NotificationActionOptionModel[]; channelOptions: NotificationActionOptionModel[];
eventTypeOptions: NotificationActionOptionModel[]; eventTypeOptions: NotificationActionOptionModel[];
discordAllowedMentionOptions: NotificationActionOptionModel[];
notificationActions: NotificationActionModel[]; notificationActions: NotificationActionModel[];
project: ProjectModel; project: ProjectModel;
status?: string | null; status?: string | null;