create-crm-tmp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -64,9 +64,6 @@ import {
|
|
|
64
64
|
Code,
|
|
65
65
|
Terminal,
|
|
66
66
|
Table as TableIcon,
|
|
67
|
-
FileCode,
|
|
68
|
-
Eye,
|
|
69
|
-
Pencil,
|
|
70
67
|
Type,
|
|
71
68
|
Quote,
|
|
72
69
|
Indent,
|
|
@@ -74,8 +71,16 @@ import {
|
|
|
74
71
|
} from 'lucide-react';
|
|
75
72
|
import { createPortal } from 'react-dom';
|
|
76
73
|
import { commandsToCommandPaletteItems, registerKeyboardShortcuts } from './ui/commands';
|
|
77
|
-
import { Select,
|
|
74
|
+
import { Select, Dialog } from './ui/components';
|
|
78
75
|
import { defaultTheme } from './ui/theme';
|
|
76
|
+
import { useAppToast } from '@/contexts/app-toast-context';
|
|
77
|
+
import { devToast } from '@/lib/utils';
|
|
78
|
+
import { assertImageFileWithinLimit } from '@/lib/editor-image-limits';
|
|
79
|
+
import { uploadEditorImage } from './editor/upload-editor-image';
|
|
80
|
+
import {
|
|
81
|
+
applyOrderedImageLayoutAfterImport,
|
|
82
|
+
mergeImgDimensionAttrsIntoStyle,
|
|
83
|
+
} from '@/lib/editor-html-image-dimensions';
|
|
79
84
|
|
|
80
85
|
type TableConfig = {
|
|
81
86
|
rows?: number;
|
|
@@ -83,6 +88,30 @@ type TableConfig = {
|
|
|
83
88
|
includeHeaders?: boolean;
|
|
84
89
|
};
|
|
85
90
|
|
|
91
|
+
function fileToDataUrl(file: File): Promise<string> {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const reader = new FileReader();
|
|
94
|
+
reader.onload = () => {
|
|
95
|
+
const result = reader.result;
|
|
96
|
+
if (typeof result === 'string') {
|
|
97
|
+
resolve(result);
|
|
98
|
+
} else {
|
|
99
|
+
reject(new Error('Impossible de convertir le fichier en data URL'));
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
reader.onerror = () =>
|
|
103
|
+
reject(reader.error ?? new Error('Erreur de lecture du fichier image'));
|
|
104
|
+
reader.readAsDataURL(file);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function imageFileToDataUrl(file: File, maxImageBytes?: number): Promise<string> {
|
|
109
|
+
if (maxImageBytes !== undefined) {
|
|
110
|
+
assertImageFileWithinLimit(file, maxImageBytes);
|
|
111
|
+
}
|
|
112
|
+
return fileToDataUrl(file);
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
// Create markdown extension instance
|
|
87
116
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
88
117
|
// @ts-ignore - Lexical duplicate types between packages
|
|
@@ -143,16 +172,21 @@ export interface DefaultTemplateRef {
|
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
// Hook for image handling logic
|
|
146
|
-
function useImageHandlers(
|
|
175
|
+
function useImageHandlers(
|
|
176
|
+
commands: EditorCommands,
|
|
177
|
+
editor: LexicalEditor | null,
|
|
178
|
+
maxImageBytes?: number,
|
|
179
|
+
) {
|
|
147
180
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
181
|
+
const toast = useAppToast();
|
|
148
182
|
|
|
149
183
|
const handlers = useMemo(
|
|
150
184
|
() => ({
|
|
151
185
|
insertFromUrl: () => {
|
|
152
|
-
const src = prompt('
|
|
186
|
+
const src = prompt("URL de l'image (https://…) :");
|
|
153
187
|
if (!src) return;
|
|
154
|
-
const alt = prompt('
|
|
155
|
-
const caption = prompt('
|
|
188
|
+
const alt = prompt('Texte alternatif (accessibilité) :') || '';
|
|
189
|
+
const caption = prompt('Légende (optionnel) :') || undefined;
|
|
156
190
|
commands.insertImage({ src, alt, caption });
|
|
157
191
|
},
|
|
158
192
|
insertFromFile: () => fileInputRef.current?.click(),
|
|
@@ -164,11 +198,19 @@ function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null
|
|
|
164
198
|
try {
|
|
165
199
|
src = await imageExtension.config.uploadHandler(file);
|
|
166
200
|
} catch (error) {
|
|
201
|
+
// Toast déjà affiché par uploadHandler (signature / collage)
|
|
167
202
|
console.error('Failed to upload image:', error);
|
|
203
|
+
e.target.value = '';
|
|
168
204
|
return;
|
|
169
205
|
}
|
|
170
206
|
} else {
|
|
171
|
-
|
|
207
|
+
try {
|
|
208
|
+
src = await imageFileToDataUrl(file, maxImageBytes);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
toast.error(devToast("Impossible d'ajouter cette image.", error));
|
|
211
|
+
e.target.value = '';
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
172
214
|
}
|
|
173
215
|
commands.insertImage({ src, alt: file.name, file });
|
|
174
216
|
e.target.value = '';
|
|
@@ -181,7 +223,7 @@ function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null
|
|
|
181
223
|
commands.setImageCaption(newCaption);
|
|
182
224
|
},
|
|
183
225
|
}),
|
|
184
|
-
[commands],
|
|
226
|
+
[commands, maxImageBytes, toast],
|
|
185
227
|
);
|
|
186
228
|
|
|
187
229
|
return { handlers, fileInputRef };
|
|
@@ -268,6 +310,10 @@ function FloatingToolbarRenderer() {
|
|
|
268
310
|
>
|
|
269
311
|
<Type size={14} />
|
|
270
312
|
</button>
|
|
313
|
+
<div className="bg-border mx-1 h-6 w-px" />
|
|
314
|
+
<span className="text-muted-foreground px-1.5 text-[11px] leading-6 whitespace-nowrap">
|
|
315
|
+
Shift + glisser pour garder le ratio
|
|
316
|
+
</span>
|
|
271
317
|
</>
|
|
272
318
|
) : (
|
|
273
319
|
<>
|
|
@@ -406,15 +452,15 @@ function Toolbar({
|
|
|
406
452
|
commands,
|
|
407
453
|
hasExtension,
|
|
408
454
|
activeStates,
|
|
455
|
+
maxImageBytes,
|
|
409
456
|
}: {
|
|
410
457
|
commands: EditorCommands;
|
|
411
458
|
hasExtension: (name: ExtensionNames) => boolean;
|
|
412
459
|
activeStates: EditorStateQueries;
|
|
460
|
+
maxImageBytes?: number;
|
|
413
461
|
}) {
|
|
414
462
|
const { lexical: editor } = useEditor();
|
|
415
|
-
const { handlers, fileInputRef } = useImageHandlers(commands, editor);
|
|
416
|
-
const [showImageDropdown, setShowImageDropdown] = useState(false);
|
|
417
|
-
const [showAlignDropdown, setShowAlignDropdown] = useState(false);
|
|
463
|
+
const { handlers, fileInputRef } = useImageHandlers(commands, editor, maxImageBytes);
|
|
418
464
|
const [showTableDialog, setShowTableDialog] = useState(false);
|
|
419
465
|
const [tableConfig, setTableConfig] = useState<TableConfig>({
|
|
420
466
|
rows: 3,
|
|
@@ -589,6 +635,35 @@ function Toolbar({
|
|
|
589
635
|
</div>
|
|
590
636
|
)}
|
|
591
637
|
|
|
638
|
+
{/* Image : fichier local ou URL (handlers étaient définis mais non exposés dans la barre) */}
|
|
639
|
+
{hasExtension('image') && (
|
|
640
|
+
<div className="lexkit-toolbar-section">
|
|
641
|
+
<input
|
|
642
|
+
ref={fileInputRef}
|
|
643
|
+
type="file"
|
|
644
|
+
accept="image/*"
|
|
645
|
+
className="hidden"
|
|
646
|
+
onChange={handlers.handleUpload}
|
|
647
|
+
/>
|
|
648
|
+
<button
|
|
649
|
+
type="button"
|
|
650
|
+
onClick={() => handlers.insertFromFile()}
|
|
651
|
+
className="lexkit-toolbar-button"
|
|
652
|
+
title="Insérer une image depuis un fichier"
|
|
653
|
+
>
|
|
654
|
+
<Upload size={16} />
|
|
655
|
+
</button>
|
|
656
|
+
<button
|
|
657
|
+
type="button"
|
|
658
|
+
onClick={() => handlers.insertFromUrl()}
|
|
659
|
+
className="lexkit-toolbar-button"
|
|
660
|
+
title="Insérer une image depuis une URL"
|
|
661
|
+
>
|
|
662
|
+
<ImageIcon size={16} />
|
|
663
|
+
</button>
|
|
664
|
+
</div>
|
|
665
|
+
)}
|
|
666
|
+
|
|
592
667
|
{/* Table */}
|
|
593
668
|
{hasExtension('table') && (
|
|
594
669
|
<div className="lexkit-toolbar-section">
|
|
@@ -722,11 +797,13 @@ function EditorContent({
|
|
|
722
797
|
isDark,
|
|
723
798
|
toggleTheme,
|
|
724
799
|
onReady,
|
|
800
|
+
maxImageBytes,
|
|
725
801
|
}: {
|
|
726
802
|
className?: string;
|
|
727
803
|
isDark: boolean;
|
|
728
804
|
toggleTheme: () => void;
|
|
729
805
|
onReady?: (methods: DefaultTemplateRef) => void;
|
|
806
|
+
maxImageBytes?: number;
|
|
730
807
|
}) {
|
|
731
808
|
const { commands, hasExtension, activeStates, lexical: editor } = useEditor();
|
|
732
809
|
const commandsRef = useRef<EditorCommands>(commands);
|
|
@@ -752,10 +829,46 @@ function EditorContent({
|
|
|
752
829
|
},
|
|
753
830
|
injectHTML: (content: string) => {
|
|
754
831
|
setTimeout(() => {
|
|
755
|
-
if (editor)
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
832
|
+
if (!editor) return;
|
|
833
|
+
const safeContent = typeof content === 'string' ? content.trim() : '';
|
|
834
|
+
const containsHtmlTag = /<[^>]+>/.test(safeContent);
|
|
835
|
+
const plainParagraph = `<p>${safeContent
|
|
836
|
+
.replaceAll('&', '&')
|
|
837
|
+
.replaceAll('<', '<')
|
|
838
|
+
.replaceAll('>', '>')
|
|
839
|
+
.replaceAll('"', '"')
|
|
840
|
+
.replaceAll("'", ''')
|
|
841
|
+
.replaceAll('\n', '<br />')}</p>`;
|
|
842
|
+
|
|
843
|
+
const contentForImport = !safeContent
|
|
844
|
+
? '<p></p>'
|
|
845
|
+
: containsHtmlTag
|
|
846
|
+
? mergeImgDimensionAttrsIntoStyle(safeContent)
|
|
847
|
+
: plainParagraph;
|
|
848
|
+
|
|
849
|
+
const afterImport = () => {
|
|
850
|
+
if (!containsHtmlTag || !contentForImport.includes('<img')) return;
|
|
851
|
+
applyOrderedImageLayoutAfterImport(editor, contentForImport);
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
const result = commandsRef.current.importFromHTML(contentForImport, {
|
|
856
|
+
preventFocus: true,
|
|
857
|
+
}) as Promise<void> | void;
|
|
858
|
+
|
|
859
|
+
if (result && typeof (result as Promise<void>).then === 'function') {
|
|
860
|
+
(result as Promise<void>)
|
|
861
|
+
.then(afterImport)
|
|
862
|
+
.catch((error) => {
|
|
863
|
+
console.error('Erreur import HTML éditeur, fallback contenu vide:', error);
|
|
864
|
+
void commandsRef.current.importFromHTML('<p></p>', { preventFocus: true });
|
|
865
|
+
});
|
|
866
|
+
} else {
|
|
867
|
+
afterImport();
|
|
868
|
+
}
|
|
869
|
+
} catch (error) {
|
|
870
|
+
console.error('Erreur injectHTML éditeur:', error);
|
|
871
|
+
void commandsRef.current.importFromHTML('<p></p>', { preventFocus: true });
|
|
759
872
|
}
|
|
760
873
|
}, 100);
|
|
761
874
|
},
|
|
@@ -774,7 +887,14 @@ function EditorContent({
|
|
|
774
887
|
}
|
|
775
888
|
},
|
|
776
889
|
getMarkdown: () => commandsRef.current.exportToMarkdown(),
|
|
777
|
-
getHTML: () =>
|
|
890
|
+
getHTML: () => {
|
|
891
|
+
try {
|
|
892
|
+
const raw = commandsRef.current.exportToHTML() || '';
|
|
893
|
+
return mergeImgDimensionAttrsIntoStyle(raw);
|
|
894
|
+
} catch {
|
|
895
|
+
return commandsRef.current.exportToHTML() || '';
|
|
896
|
+
}
|
|
897
|
+
},
|
|
778
898
|
}),
|
|
779
899
|
[editor],
|
|
780
900
|
);
|
|
@@ -803,7 +923,12 @@ function EditorContent({
|
|
|
803
923
|
return (
|
|
804
924
|
<>
|
|
805
925
|
<div className="lexkit-editor-header">
|
|
806
|
-
<Toolbar
|
|
926
|
+
<Toolbar
|
|
927
|
+
commands={commands}
|
|
928
|
+
hasExtension={hasExtension}
|
|
929
|
+
activeStates={activeStates}
|
|
930
|
+
maxImageBytes={maxImageBytes}
|
|
931
|
+
/>
|
|
807
932
|
</div>
|
|
808
933
|
<div className="lexkit-editor">
|
|
809
934
|
<div className="flex flex-1 flex-col" style={{ display: 'flex' }}>
|
|
@@ -823,23 +948,36 @@ function EditorContent({
|
|
|
823
948
|
interface DefaultTemplateProps {
|
|
824
949
|
className?: string;
|
|
825
950
|
onReady?: (methods: DefaultTemplateRef) => void;
|
|
951
|
+
/** Si défini, refuse les images plus lourdes (fichier + collage). */
|
|
952
|
+
maxImageBytes?: number;
|
|
826
953
|
}
|
|
827
954
|
|
|
828
955
|
export const Editor = forwardRef<DefaultTemplateRef, DefaultTemplateProps>(
|
|
829
|
-
({ className, onReady }, ref) => {
|
|
956
|
+
({ className, onReady, maxImageBytes }, ref) => {
|
|
830
957
|
const [editorTheme, setEditorTheme] = useState<'light' | 'dark'>('light');
|
|
958
|
+
const toast = useAppToast();
|
|
831
959
|
|
|
832
960
|
const isDark = editorTheme === 'dark';
|
|
833
961
|
|
|
834
962
|
useEffect(() => {
|
|
835
963
|
imageExtension.configure({
|
|
836
|
-
uploadHandler: async (file: File) =>
|
|
964
|
+
uploadHandler: async (file: File) => {
|
|
965
|
+
if (maxImageBytes !== undefined) {
|
|
966
|
+
assertImageFileWithinLimit(file, maxImageBytes);
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
return await uploadEditorImage(file);
|
|
970
|
+
} catch {
|
|
971
|
+
// Fallback: data URL (base64)
|
|
972
|
+
return imageFileToDataUrl(file, maxImageBytes);
|
|
973
|
+
}
|
|
974
|
+
},
|
|
837
975
|
defaultAlignment: 'center',
|
|
838
976
|
resizable: true,
|
|
839
977
|
pasteListener: { insert: true, replace: true },
|
|
840
978
|
debug: false,
|
|
841
979
|
});
|
|
842
|
-
}, []);
|
|
980
|
+
}, [maxImageBytes, toast]);
|
|
843
981
|
|
|
844
982
|
const toggleTheme = () => setEditorTheme(isDark ? 'light' : 'dark');
|
|
845
983
|
|
|
@@ -860,6 +998,7 @@ export const Editor = forwardRef<DefaultTemplateRef, DefaultTemplateProps>(
|
|
|
860
998
|
isDark={isDark}
|
|
861
999
|
toggleTheme={toggleTheme}
|
|
862
1000
|
onReady={handleReady}
|
|
1001
|
+
maxImageBytes={maxImageBytes}
|
|
863
1002
|
/>
|
|
864
1003
|
</Provider>
|
|
865
1004
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
|
|
2
2
|
|
|
3
3
|
interface EmailTemplateProps {
|
|
4
4
|
firstName: string;
|
|
@@ -29,7 +29,7 @@ export function EmailTemplate({ firstName, signature }: EmailTemplateProps) {
|
|
|
29
29
|
fontSize: '14px',
|
|
30
30
|
lineHeight: '1.6',
|
|
31
31
|
}}
|
|
32
|
-
dangerouslySetInnerHTML={{ __html:
|
|
32
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
|
|
33
33
|
/>
|
|
34
34
|
)}
|
|
35
35
|
</div>
|
|
@@ -110,9 +110,7 @@ export function GlobalSearch() {
|
|
|
110
110
|
|
|
111
111
|
const hasResults =
|
|
112
112
|
filteredPages.length > 0 ||
|
|
113
|
-
(crmResults &&
|
|
114
|
-
(crmResults.contacts.length > 0 ||
|
|
115
|
-
crmResults.companies.length > 0));
|
|
113
|
+
(crmResults && (crmResults.contacts.length > 0 || crmResults.companies.length > 0));
|
|
116
114
|
|
|
117
115
|
const navigate = useCallback(
|
|
118
116
|
(href: string) => {
|
|
@@ -169,7 +167,7 @@ export function GlobalSearch() {
|
|
|
169
167
|
return (
|
|
170
168
|
<div ref={containerRef} className="relative hidden w-full max-w-xl sm:block">
|
|
171
169
|
<div className="relative">
|
|
172
|
-
<Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2
|
|
170
|
+
<Search aria-hidden="true" className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
|
173
171
|
<input
|
|
174
172
|
ref={inputRef}
|
|
175
173
|
type="text"
|
|
@@ -182,8 +180,8 @@ export function GlobalSearch() {
|
|
|
182
180
|
if (query.trim().length > 0) setIsOpen(true);
|
|
183
181
|
}}
|
|
184
182
|
onKeyDown={handleKeyDown}
|
|
185
|
-
placeholder="Rechercher
|
|
186
|
-
className="
|
|
183
|
+
placeholder="Rechercher..."
|
|
184
|
+
className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary/40 focus-visible:bg-background focus-visible:ring-primary/40 w-full rounded-lg border py-2 pr-4 pl-9 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
187
185
|
role="combobox"
|
|
188
186
|
aria-expanded={showDropdown}
|
|
189
187
|
aria-controls="global-search-results"
|
|
@@ -200,13 +198,13 @@ export function GlobalSearch() {
|
|
|
200
198
|
<div
|
|
201
199
|
id="global-search-results"
|
|
202
200
|
role="listbox"
|
|
203
|
-
className="absolute top-full left-0 z-50 mt-1 max-h-112 w-full overflow-y-auto rounded-xl border
|
|
201
|
+
className="border-border bg-popover ui-dropdown-enter absolute top-full left-0 z-50 mt-1 max-h-112 w-full overflow-y-auto rounded-xl border shadow-(--shadow-dropdown)"
|
|
204
202
|
>
|
|
205
203
|
{/* Pages */}
|
|
206
204
|
{filteredPages.length > 0 && (
|
|
207
205
|
<div>
|
|
208
206
|
<div className="px-3 pt-3 pb-1">
|
|
209
|
-
<span className="text-xs font-semibold tracking-wider
|
|
207
|
+
<span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
|
210
208
|
Pages
|
|
211
209
|
</span>
|
|
212
210
|
</div>
|
|
@@ -222,17 +220,19 @@ export function GlobalSearch() {
|
|
|
222
220
|
id={`global-search-option-${idx}`}
|
|
223
221
|
onClick={() => navigate(page.href)}
|
|
224
222
|
className={cn(
|
|
225
|
-
'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm
|
|
223
|
+
'text-popover-foreground hover:bg-accent flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200',
|
|
226
224
|
focusedIndex === idx && 'bg-accent',
|
|
227
225
|
)}
|
|
228
226
|
role="option"
|
|
229
227
|
aria-selected={focusedIndex === idx}
|
|
230
228
|
>
|
|
231
|
-
<Icon className="h-4 w-4 shrink-0
|
|
229
|
+
<Icon aria-hidden="true" className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
232
230
|
<div className="min-w-0 flex-1">
|
|
233
|
-
<span className="block truncate font-medium
|
|
231
|
+
<span className="text-popover-foreground block truncate font-medium">
|
|
232
|
+
{page.name}
|
|
233
|
+
</span>
|
|
234
234
|
{page.parentLabel && (
|
|
235
|
-
<span className="block truncate text-xs
|
|
235
|
+
<span className="text-muted-foreground block truncate text-xs">
|
|
236
236
|
{page.parentLabel} {'>'} {page.name}
|
|
237
237
|
</span>
|
|
238
238
|
)}
|
|
@@ -245,7 +245,7 @@ export function GlobalSearch() {
|
|
|
245
245
|
|
|
246
246
|
{/* CRM loading */}
|
|
247
247
|
{shouldFetchCRM && crmLoading && !crmResults && (
|
|
248
|
-
<div className="flex items-center justify-center gap-2 px-3 py-6 text-sm
|
|
248
|
+
<div className="text-muted-foreground flex items-center justify-center gap-2 px-3 py-6 text-sm">
|
|
249
249
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
250
250
|
Recherche...
|
|
251
251
|
</div>
|
|
@@ -257,10 +257,10 @@ export function GlobalSearch() {
|
|
|
257
257
|
<div
|
|
258
258
|
className={cn(
|
|
259
259
|
'px-3 pt-3 pb-1',
|
|
260
|
-
filteredPages.length > 0 && 'border-
|
|
260
|
+
filteredPages.length > 0 && 'border-border border-t',
|
|
261
261
|
)}
|
|
262
262
|
>
|
|
263
|
-
<span className="text-xs font-semibold tracking-wider
|
|
263
|
+
<span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
|
264
264
|
Contacts
|
|
265
265
|
</span>
|
|
266
266
|
</div>
|
|
@@ -278,17 +278,17 @@ export function GlobalSearch() {
|
|
|
278
278
|
id={`global-search-option-${idx}`}
|
|
279
279
|
onClick={() => navigate(`/contacts/${contact.id}`)}
|
|
280
280
|
className={cn(
|
|
281
|
-
'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200
|
|
281
|
+
'hover:bg-accent flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200',
|
|
282
282
|
focusedIndex === idx && 'bg-accent',
|
|
283
283
|
)}
|
|
284
284
|
role="option"
|
|
285
285
|
aria-selected={focusedIndex === idx}
|
|
286
286
|
>
|
|
287
|
-
<Users className="h-4 w-4 shrink-0
|
|
287
|
+
<Users aria-hidden="true" className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
288
288
|
<div className="min-w-0 flex-1">
|
|
289
|
-
<span className="
|
|
289
|
+
<span className="text-popover-foreground font-medium">{name}</span>
|
|
290
290
|
{(contact.email || contact.phone) && (
|
|
291
|
-
<span className="ml-2 truncate text-xs
|
|
291
|
+
<span className="text-muted-foreground ml-2 truncate text-xs">
|
|
292
292
|
{contact.email || contact.phone}
|
|
293
293
|
</span>
|
|
294
294
|
)}
|
|
@@ -302,8 +302,8 @@ export function GlobalSearch() {
|
|
|
302
302
|
{/* Entreprises */}
|
|
303
303
|
{crmResults && crmResults.companies.length > 0 && (
|
|
304
304
|
<div>
|
|
305
|
-
<div className="border-
|
|
306
|
-
<span className="text-xs font-semibold tracking-wider
|
|
305
|
+
<div className="border-border border-t px-3 pt-3 pb-1">
|
|
306
|
+
<span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
|
307
307
|
Entreprises
|
|
308
308
|
</span>
|
|
309
309
|
</div>
|
|
@@ -318,17 +318,19 @@ export function GlobalSearch() {
|
|
|
318
318
|
id={`global-search-option-${idx}`}
|
|
319
319
|
onClick={() => navigate(`/contacts/companies/${company.id}`)}
|
|
320
320
|
className={cn(
|
|
321
|
-
'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200
|
|
321
|
+
'hover:bg-accent flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200',
|
|
322
322
|
focusedIndex === idx && 'bg-accent',
|
|
323
323
|
)}
|
|
324
324
|
role="option"
|
|
325
325
|
aria-selected={focusedIndex === idx}
|
|
326
326
|
>
|
|
327
|
-
<Building2 className="h-4 w-4 shrink-0
|
|
327
|
+
<Building2 aria-hidden="true" className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
328
328
|
<div className="min-w-0 flex-1">
|
|
329
|
-
<span className="
|
|
329
|
+
<span className="text-popover-foreground font-medium">{company.name}</span>
|
|
330
330
|
{company.email && (
|
|
331
|
-
<span className="ml-2 truncate text-xs
|
|
331
|
+
<span className="text-muted-foreground ml-2 truncate text-xs">
|
|
332
|
+
{company.email}
|
|
333
|
+
</span>
|
|
332
334
|
)}
|
|
333
335
|
</div>
|
|
334
336
|
</button>
|
|
@@ -339,15 +341,15 @@ export function GlobalSearch() {
|
|
|
339
341
|
|
|
340
342
|
{/* Aucun résultat */}
|
|
341
343
|
{!crmLoading && !hasResults && query.trim().length > 0 && (
|
|
342
|
-
<div className="px-3 py-6 text-center text-sm
|
|
344
|
+
<div className="text-muted-foreground px-3 py-6 text-center text-sm">
|
|
343
345
|
Aucun résultat pour "{query.trim()}"
|
|
344
346
|
</div>
|
|
345
347
|
)}
|
|
346
348
|
|
|
347
349
|
{/* Recherche trop courte pour le CRM */}
|
|
348
350
|
{query.trim().length === 1 && filteredPages.length === 0 && (
|
|
349
|
-
<div className="px-3 py-6 text-center text-sm
|
|
350
|
-
<FileText className="mx-auto mb-1 h-5 w-5
|
|
351
|
+
<div className="text-muted-foreground px-3 py-6 text-center text-sm">
|
|
352
|
+
<FileText aria-hidden="true" className="text-muted-foreground/70 mx-auto mb-1 h-5 w-5" />
|
|
351
353
|
Tapez au moins 2 caractères pour rechercher dans le CRM
|
|
352
354
|
</div>
|
|
353
355
|
)}
|