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:
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user