Refactor embed handling and enhance user experience across components

- Integrated the new `buildEmbedFromPayload` function in Scheduler, Tags, and Welcome components to streamline embed creation and management.
- Updated validation schemas to require embed content checks, ensuring robust data integrity for embeds.
- Enhanced localization files to include new placeholder descriptions and improve user guidance for embed fields.
- Refactored embed parsing logic to utilize the `WelcomeEmbedSchema`, improving consistency and validation across the application.
This commit is contained in:
TheOnlyMace
2026-07-22 22:30:46 +02:00
parent e7a3162494
commit 95eed45ec4
19 changed files with 500 additions and 146 deletions

View File

@@ -1,5 +1,5 @@
import { z } from 'zod';
import { WelcomeEmbedSchema } from './phase2.js';
import { embedHasContent, WelcomeEmbedSchema } from './phase2.js';
import {
SelfRoleBehaviorSchema,
SelfRoleEntrySchema,
@@ -406,9 +406,9 @@ export type TagDashboard = z.infer<typeof TagDashboardSchema>;
export const TagDashboardCreateSchema = TagDashboardSchema.omit({ id: true }).refine(
(value) =>
value.responseType !== 'EMBED' ||
Boolean(value.embed?.title?.trim()) ||
Boolean(value.embed?.description?.trim()),
{ message: 'Embed title or description is required for EMBED tags', path: ['embed'] }
embedHasContent(value.embed) ||
value.embed?.color !== undefined,
{ message: 'Embed content is required for EMBED tags', path: ['embed'] }
);
export type TagDashboardCreate = z.infer<typeof TagDashboardCreateSchema>;
@@ -587,8 +587,8 @@ export const ScheduledMessageDashboardCreateSchema = z
.refine(
(value) =>
Boolean(value.content?.trim()) ||
Boolean(value.embed?.title?.trim()) ||
Boolean(value.embed?.description?.trim()),
embedHasContent(value.embed) ||
value.embed?.color !== undefined,
{ message: 'Content or embed is required' }
);
export type ScheduledMessageDashboardCreate = z.infer<typeof ScheduledMessageDashboardCreateSchema>;

View File

@@ -27,6 +27,19 @@ describe('phase2 helpers', () => {
expect(result).toBe('Join <#123456789012345678> please');
});
it('accepts extended embed fields', async () => {
const { WelcomeEmbedSchema, embedHasContent } = await import('./phase2.js');
const parsed = WelcomeEmbedSchema.parse({
title: 'Hi',
authorName: 'Nexumi',
imageUrl: 'https://example.com/banner.png',
footerText: 'Nexumi',
timestamp: true
});
expect(parsed.authorName).toBe('Nexumi');
expect(embedHasContent(parsed)).toBe(true);
});
it('detects discord invites', () => {
expect(isDiscordInvite('join https://discord.gg/test')).toBe(true);
expect(isDiscordInvite('hello world')).toBe(false);

View File

@@ -131,12 +131,73 @@ export const WelcomeMessageTypeSchema = z.enum(['TEXT', 'EMBED', 'IMAGE']);
export type WelcomeMessageType = z.infer<typeof WelcomeMessageTypeSchema>;
export const WelcomeEmbedSchema = z.object({
title: z.string().optional(),
description: z.string().optional(),
color: z.number().int().optional()
title: z.string().max(256).optional(),
description: z.string().max(4096).optional(),
/** Clickable title URL. */
url: z.string().max(2048).optional(),
color: z.number().int().optional(),
authorName: z.string().max(256).optional(),
authorIconUrl: z.string().max(2048).optional(),
authorUrl: z.string().max(2048).optional(),
thumbnailUrl: z.string().max(2048).optional(),
imageUrl: z.string().max(2048).optional(),
footerText: z.string().max(2048).optional(),
footerIconUrl: z.string().max(2048).optional(),
/** When true, Discord shows the current time in the footer area. */
timestamp: z.boolean().optional()
});
export type WelcomeEmbed = z.infer<typeof WelcomeEmbedSchema>;
/** True when the embed has any user-facing content beyond an optional color. */
export function embedHasContent(embed: WelcomeEmbed | null | undefined): boolean {
if (!embed) {
return false;
}
return Boolean(
embed.title?.trim() ||
embed.description?.trim() ||
embed.url?.trim() ||
embed.authorName?.trim() ||
embed.authorIconUrl?.trim() ||
embed.authorUrl?.trim() ||
embed.thumbnailUrl?.trim() ||
embed.imageUrl?.trim() ||
embed.footerText?.trim() ||
embed.footerIconUrl?.trim() ||
embed.timestamp
);
}
/**
* Applies a string renderer to every text/URL field of an embed payload.
* Used by Welcome/Tags/Scheduler before handing data to discord.js.
*/
export function mapEmbedTextFields(
embed: WelcomeEmbed,
render: (value: string) => string
): WelcomeEmbed {
const map = (value: string | undefined): string | undefined => {
if (!value?.trim()) {
return undefined;
}
return render(value);
};
return {
title: map(embed.title),
description: map(embed.description),
url: map(embed.url),
color: embed.color,
authorName: map(embed.authorName),
authorIconUrl: map(embed.authorIconUrl),
authorUrl: map(embed.authorUrl),
thumbnailUrl: map(embed.thumbnailUrl),
imageUrl: map(embed.imageUrl),
footerText: map(embed.footerText),
footerIconUrl: map(embed.footerIconUrl),
timestamp: embed.timestamp
};
}
export const VerificationModeSchema = z.enum(['BUTTON', 'CAPTCHA']);
export type VerificationMode = z.infer<typeof VerificationModeSchema>;