feat(ui): modals close only via Esc/X/cancel; standard 3:1 form cover processing

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 15:23:14 +03:30
co-authored by Cursor
parent f5b6a47f3c
commit 2f3646d085
19 changed files with 366 additions and 117 deletions
@@ -106,7 +106,7 @@ export class LeadFormsController {
const form = await this.leadFormsService.get(user.organizationId, id);
const row = await this.leadFormsService.getByToken(form.publicToken);
if (!row?.coverStorageKey) return { ok: false };
res.set({ 'Content-Type': 'image/jpeg' });
res.set({ 'Content-Type': this.leadFormsService.coverContentType(row.coverStorageKey) });
return new StreamableFile(this.files.openReadStream(row.coverStorageKey));
}
+17 -2
View File
@@ -25,6 +25,13 @@ import {
} from '../contacts/normalize';
import { LeadStagesService } from '../leads/lead-stages.service';
const COVER_CONTENT_TYPES: Record<string, string> = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
webp: 'image/webp',
};
const DEFAULT_FIELDS: LeadFormFieldInput[] = [
{
key: 'fullName',
@@ -222,7 +229,7 @@ export class LeadFormsService {
submitColor: row.submitColor || '#14b8a6',
hasCover: Boolean(row.coverStorageKey),
coverPath: row.coverStorageKey
? `/api/public/forms/${row.publicToken}/cover`
? `/api/public/forms/${row.publicToken}/cover?v=${row.updatedAt.getTime()}`
: null,
fields: row.fields.map((f) => ({
key: f.key,
@@ -806,7 +813,10 @@ export class LeadFormsService {
.toLowerCase()
.replace(/[^a-z0-9]/g, '')
.slice(0, 6);
const key = `lead-forms/${id}/cover.${ext || 'jpg'}`;
if (!COVER_CONTENT_TYPES[ext]) {
throw new BadRequestException('فرمت تصویر کاور باید PNG، JPG یا WEBP باشد');
}
const key = `lead-forms/${id}/cover.${ext}`;
await this.files.save(key, file.buffer);
if (row.coverStorageKey && row.coverStorageKey !== key) {
await this.files.remove(row.coverStorageKey);
@@ -832,6 +842,11 @@ export class LeadFormsService {
return row.coverStorageKey;
}
coverContentType(key: string) {
const ext = key.split('.').pop()?.toLowerCase() ?? '';
return COVER_CONTENT_TYPES[ext] ?? 'image/jpeg';
}
async getByToken(token: string, onlyActive = false) {
return this.prisma.leadForm.findFirst({
where: { publicToken: token, ...(onlyActive ? { isActive: true } : {}) },
@@ -33,7 +33,10 @@ export class PublicLeadFormsController {
) {
const row = await this.leadFormsService.getByToken(token);
if (!row?.coverStorageKey) throw new NotFoundException('کاور پیدا نشد');
res.set({ 'Content-Type': 'image/jpeg', 'Cache-Control': 'public, max-age=3600' });
res.set({
'Content-Type': this.leadFormsService.coverContentType(row.coverStorageKey),
'Cache-Control': 'public, max-age=3600',
});
return new StreamableFile(this.files.openReadStream(row.coverStorageKey));
}