From 2f3646d0858109a8d3b205dfd0275df7d6542259 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Wed, 7 Oct 2026 15:23:14 +0330 Subject: [PATCH] feat(ui): modals close only via Esc/X/cancel; standard 3:1 form cover processing Co-authored-by: Cursor --- .../src/lead-forms/lead-forms.controller.ts | 2 +- apps/api/src/lead-forms/lead-forms.service.ts | 19 ++- .../public-lead-forms.controller.ts | 5 +- .../src/components/MessageIntervalPicker.vue | 11 +- apps/web/src/components/SavedFiltersBar.vue | 16 +- .../activities/ActivityCommentSheet.vue | 2 +- .../activities/ActivityPostponeSheet.vue | 2 +- apps/web/src/components/ui/UiDrawer.vue | 21 +-- apps/web/src/components/ui/UiModal.vue | 27 +--- apps/web/src/lib/form-cover.ts | 72 +++++++++ apps/web/src/lib/modal-stack.ts | 49 ++++++ apps/web/src/views/ConversationsView.vue | 68 ++++++-- apps/web/src/views/InvoicesView.vue | 8 +- apps/web/src/views/LeadFormBuilderView.vue | 149 ++++++++++++++---- apps/web/src/views/LeadFormPublicView.vue | 2 +- apps/web/src/views/LoyaltyClubView.vue | 7 +- apps/web/src/views/MessageAutomationsView.vue | 8 +- apps/web/src/views/NotesView.vue | 11 +- apps/web/src/views/TeamSettingsView.vue | 4 +- 19 files changed, 366 insertions(+), 117 deletions(-) create mode 100644 apps/web/src/lib/form-cover.ts create mode 100644 apps/web/src/lib/modal-stack.ts diff --git a/apps/api/src/lead-forms/lead-forms.controller.ts b/apps/api/src/lead-forms/lead-forms.controller.ts index 3572922..ca27233 100644 --- a/apps/api/src/lead-forms/lead-forms.controller.ts +++ b/apps/api/src/lead-forms/lead-forms.controller.ts @@ -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)); } diff --git a/apps/api/src/lead-forms/lead-forms.service.ts b/apps/api/src/lead-forms/lead-forms.service.ts index f68c2d1..16dc598 100644 --- a/apps/api/src/lead-forms/lead-forms.service.ts +++ b/apps/api/src/lead-forms/lead-forms.service.ts @@ -25,6 +25,13 @@ import { } from '../contacts/normalize'; import { LeadStagesService } from '../leads/lead-stages.service'; +const COVER_CONTENT_TYPES: Record = { + 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 } : {}) }, diff --git a/apps/api/src/lead-forms/public-lead-forms.controller.ts b/apps/api/src/lead-forms/public-lead-forms.controller.ts index dd9aa42..157ec0d 100644 --- a/apps/api/src/lead-forms/public-lead-forms.controller.ts +++ b/apps/api/src/lead-forms/public-lead-forms.controller.ts @@ -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)); } diff --git a/apps/web/src/components/MessageIntervalPicker.vue b/apps/web/src/components/MessageIntervalPicker.vue index aa3a6e8..a5e9001 100644 --- a/apps/web/src/components/MessageIntervalPicker.vue +++ b/apps/web/src/components/MessageIntervalPicker.vue @@ -1,5 +1,6 @@ @@ -116,7 +113,7 @@ onUnmounted(() => {
- +