PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / src / admin / dashboard / pages / Emails.js
presto-player / src / admin / dashboard / pages Last commit date
learn 1 month ago settings 1 month ago Analytics.js 1 month ago AnalyticsDetail.js 1 month ago Dashboard.js 1 month ago Emails.js 1 month ago Learn.js 1 month ago MediaHub.js 3 weeks ago Onboarding.js 3 weeks ago Settings.js 1 month ago UserAnalyticsDetail.js 1 month ago
Emails.js
770 lines
1 /**
2 * Emails dashboard page – list, filter, add/edit email submissions.
3 * Matches Presto Player / MediaHub patterns: useEmail hook, shared components, toast, ConfirmPopup.
4 *
5 * Structure: This page composes components from ../components/Emails (see components/Emails/README.md).
6 * Data: useEmail (hooks/useEmail.ts). Popups: EmailFormPopup, ConfirmPopup. Shared: MediaHub/BulkActions.
7 */
8 // eslint-disable-next-line import/no-extraneous-dependencies
9 import React, { useState, useRef, useMemo, useEffect } from 'react';
10 const { __, _n, sprintf } = wp.i18n;
11 import apiFetch from '@wordpress/api-fetch';
12 import '../../../styles/tailwind.css';
13 import { Container, DropdownMenu, toast } from '@bsf/force-ui';
14 import useEmail, { EMAIL_SUBMISSIONS_PATH } from '../hooks/useEmail.ts';
15 import BulkActions from '../components/MediaHub/BulkActions';
16 import ConfirmPopup from '../components/Popup/ConfirmPopup';
17 import EmailFormPopup from '../components/Popup/EmailFormPopup';
18 import Filters from '../components/Filters';
19 import PageHeader from '../components/PageHeader';
20 import {
21 Table,
22 statusOptions,
23 togglePageSelection,
24 } from '../components/Emails';
25 import MediaHubPageSkeleton from '../components/Skeletons/MediaHubPageSkeleton';
26 import NoFound from '../components/NoFound';
27 import emailsEmptyState from '../../../../img/emails-empty-state.svg';
28 import {
29 Plus,
30 FolderArchive,
31 CheckCheck,
32 Trash,
33 Trash2,
34 ArchiveRestore,
35 } from 'lucide-react';
36 import ProGateOverlay from '../components/ProGateOverlay';
37
38 const POSTS_PER_PAGE = 10;
39
40 const EmailsContent = () => {
41 const {
42 emails,
43 rawEmails,
44 setRawEmails,
45 loading,
46 sortField,
47 sortOrder,
48 handleSort,
49 fetchEmails,
50 } = useEmail();
51 const [ showFilter, setShowFilter ] = useState( false );
52 const [ searchTerm, setSearchTerm ] = useState( '' );
53 const searchInputRef = useRef( null );
54 const [ selected, setSelected ] = useState( [] );
55 const [ currentPage, setCurrentPage ] = useState( 1 );
56 const [ postCount, setPostCount ] = useState( POSTS_PER_PAGE );
57 const [ openActionPopup, setOpenActionPopup ] = useState( false );
58 const [ actionPopupData, setActionPopupData ] = useState( null );
59 const [ showEmailPopup, setShowEmailPopup ] = useState( false );
60 const [ selectedEmailForEdit, setSelectedEmailForEdit ] = useState( null );
61 const [ filterMonth, setFilterMonth ] = useState( '' );
62 const [ selectedStatus, setSelectedStatus ] = useState( 'all' );
63
64 const prevFiltersRef = useRef( {
65 searchTerm: '',
66 postCount: POSTS_PER_PAGE,
67 filterMonth: '',
68 selectedStatus: 'all',
69 } );
70
71 const monthFilterOptions = useMemo( () => {
72 const options = [
73 { value: '', label: __( 'All dates', 'presto-player' ) },
74 ];
75 const monthNames = [
76 __( 'January', 'presto-player' ),
77 __( 'February', 'presto-player' ),
78 __( 'March', 'presto-player' ),
79 __( 'April', 'presto-player' ),
80 __( 'May', 'presto-player' ),
81 __( 'June', 'presto-player' ),
82 __( 'July', 'presto-player' ),
83 __( 'August', 'presto-player' ),
84 __( 'September', 'presto-player' ),
85 __( 'October', 'presto-player' ),
86 __( 'November', 'presto-player' ),
87 __( 'December', 'presto-player' ),
88 ];
89 const seen = new Set();
90 ( rawEmails || [] ).forEach( ( item ) => {
91 if ( ! item.date ) {
92 return;
93 }
94 const d = new Date( item.date );
95 // Skip invalid dates so month filter only shows valid options; no throw.
96 if ( isNaN( d.getTime() ) ) {
97 return;
98 }
99 const key = `${ d.getFullYear() }-${ String( d.getMonth() + 1 ).padStart(
100 2,
101 '0'
102 ) }`;
103 if ( seen.has( key ) ) {
104 return;
105 }
106 seen.add( key );
107 const label = `${ monthNames[ d.getMonth() ] } ${ d.getFullYear() }`;
108 options.push( { value: key, label } );
109 } );
110 options.sort( ( a, b ) => {
111 if ( a.value === '' ) {
112 return -1;
113 }
114 if ( b.value === '' ) {
115 return 1;
116 }
117 return b.value.localeCompare( a.value );
118 } );
119 return options;
120 }, [ rawEmails ] );
121
122 useEffect( () => {
123 if ( ! [ 10, 25, 50, 75, 100 ].includes( postCount ) ) {
124 setPostCount( POSTS_PER_PAGE );
125 }
126 }, [ postCount ] );
127
128 useEffect( () => {
129 const contentFiltersChanged =
130 prevFiltersRef.current.searchTerm !== searchTerm ||
131 prevFiltersRef.current.filterMonth !== filterMonth ||
132 prevFiltersRef.current.selectedStatus !== selectedStatus;
133 const filtersChanged =
134 contentFiltersChanged || prevFiltersRef.current.postCount !== postCount;
135 if ( filtersChanged ) {
136 setCurrentPage( 1 );
137 prevFiltersRef.current = {
138 searchTerm,
139 postCount,
140 filterMonth,
141 selectedStatus,
142 };
143 }
144 // Drop the selection when a content filter changes so a "Delete N
145 // items" click can't act on rows the user can no longer see. A
146 // page-size change keeps every row visible, so selection survives.
147 if ( contentFiltersChanged ) {
148 setSelected( [] );
149 }
150 }, [ searchTerm, postCount, filterMonth, selectedStatus ] );
151
152 const handleClearFilters = () => {
153 setFilterMonth( '' );
154 setSelectedStatus( 'all' );
155 setCurrentPage( 1 );
156 setPostCount( POSTS_PER_PAGE );
157 };
158
159 const handleBulkDelete = async ( selectedIds ) => {
160 if ( ! selectedIds || selectedIds.length === 0 ) {
161 return;
162 }
163 try {
164 await apiFetch( {
165 path: '/presto-player/v1/email-submissions/delete',
166 method: 'POST',
167 data: { ids: selectedIds, permanent: true },
168 } );
169 await fetchEmails();
170 setSelected( [] );
171 toast.success(
172 sprintf(
173 /* translators: %d is the number of deleted items */
174 __(
175 '%d email submission(s) deleted successfully.',
176 'presto-player'
177 ),
178 selectedIds.length
179 )
180 );
181 } catch ( error ) {
182 toast.error(
183 error?.message || __( 'Failed to delete.', 'presto-player' )
184 );
185 }
186 };
187
188 const handleBulkTrash = async ( selectedIds ) => {
189 if ( ! selectedIds || selectedIds.length === 0 ) {
190 return;
191 }
192 try {
193 await apiFetch( {
194 path: '/presto-player/v1/email-submissions/delete',
195 method: 'POST',
196 data: { ids: selectedIds },
197 } );
198 await fetchEmails();
199 setSelected( [] );
200 toast.success(
201 sprintf(
202 /* translators: %d: number of items moved to trash */
203 _n(
204 '%d email submission moved to trash.',
205 '%d email submissions moved to trash.',
206 selectedIds.length,
207 'presto-player'
208 ),
209 selectedIds.length
210 )
211 );
212 } catch ( error ) {
213 toast.error(
214 error?.message || __( 'Failed to trash.', 'presto-player' )
215 );
216 }
217 };
218
219 const handleBulkCancel = () => {
220 setSelected( [] );
221 };
222
223 const handleBulkStatusChange = async ( selectedIds, status ) => {
224 if ( ! selectedIds || selectedIds.length === 0 ) {
225 return;
226 }
227 let updated = 0;
228 let failed = 0;
229 try {
230 const response = await apiFetch( {
231 path: `${ EMAIL_SUBMISSIONS_PATH }/status`,
232 method: 'POST',
233 data: { ids: selectedIds, status },
234 } );
235 updated = response?.updated ?? 0;
236 failed = response?.failed ?? ( selectedIds.length - updated );
237 } catch ( error ) {
238 // Response was lost — server may have applied the change. Refresh so the UI
239 // reflects authoritative state instead of staying stale until next interaction.
240 await fetchEmails();
241 toast.error(
242 error?.message ||
243 __( 'Failed to update email submissions.', 'presto-player' )
244 );
245 return;
246 }
247 await fetchEmails();
248 // Keep selection on partial failure so the user can retry the failed rows.
249 if ( failed === 0 ) {
250 setSelected( [] );
251 }
252 if ( updated > 0 ) {
253 toast.success(
254 sprintf(
255 /* translators: 1: number of items updated, 2: new status (publish or draft) */
256 _n(
257 '%1$d email submission updated to %2$s.',
258 '%1$d email submissions updated to %2$s.',
259 updated,
260 'presto-player'
261 ),
262 updated,
263 status
264 )
265 );
266 }
267 if ( failed > 0 ) {
268 toast.error(
269 sprintf(
270 /* translators: %d: number of items that failed to update */
271 _n(
272 'Failed to update %d email submission.',
273 'Failed to update %d email submissions.',
274 failed,
275 'presto-player'
276 ),
277 failed
278 )
279 );
280 }
281 };
282
283 const handleMenuActions = ( id, action ) => {
284 const item = ( rawEmails || [] ).find( ( e ) => e.id === id );
285
286 switch ( action ) {
287 case 'edit':
288 setSelectedEmailForEdit( item );
289 setShowEmailPopup( true );
290 return;
291
292 case 'trash':
293 setActionPopupData( {
294 title: __( 'Move to Trash?', 'presto-player' ),
295 description: __(
296 'Are you sure you want to move this email submission to the trash?',
297 'presto-player'
298 ),
299 confirmText: __( 'Move to Trash', 'presto-player' ),
300 destructive: true,
301 confirmCallback: async () => {
302 try {
303 await apiFetch( {
304 path: '/presto-player/v1/email-submissions/delete',
305 method: 'POST',
306 data: { ids: [ id ] },
307 } );
308 await fetchEmails();
309 toast.success(
310 __( 'Successfully trashed.', 'presto-player' )
311 );
312 } catch ( error ) {
313 toast.error(
314 error?.message ||
315 __( 'Failed to trash.', 'presto-player' )
316 );
317 }
318 setOpenActionPopup( false );
319 },
320 cancelCallback: () => setOpenActionPopup( false ),
321 } );
322 setOpenActionPopup( true );
323 return;
324
325 case 'delete':
326 setActionPopupData( {
327 title: __( 'Delete?', 'presto-player' ),
328 description: __(
329 'Are you sure you want to delete this email submission? This action cannot be undone.',
330 'presto-player'
331 ),
332 confirmText: __( 'Delete', 'presto-player' ),
333 destructive: true,
334 confirmCallback: async () => {
335 try {
336 await apiFetch( {
337 path: '/presto-player/v1/email-submissions/delete',
338 method: 'POST',
339 data: { ids: [ id ], permanent: true },
340 } );
341 await fetchEmails();
342 toast.success(
343 __(
344 'Email submission deleted successfully.',
345 'presto-player'
346 )
347 );
348 } catch ( error ) {
349 toast.error(
350 error?.message ||
351 __( 'Failed to delete.', 'presto-player' )
352 );
353 }
354 setOpenActionPopup( false );
355 },
356 cancelCallback: () => setOpenActionPopup( false ),
357 } );
358 setOpenActionPopup( true );
359 return;
360
361 case 'restore':
362 setActionPopupData( {
363 title: __( 'Restore?', 'presto-player' ),
364 description: __(
365 'Are you sure you want to restore this email submission?',
366 'presto-player'
367 ),
368 confirmText: __( 'Restore', 'presto-player' ),
369 confirmCallback: async () => {
370 try {
371 await apiFetch( {
372 path: `/presto-player/v1/email-submissions/${ id }`,
373 method: 'PUT',
374 data: { status: 'publish' },
375 } );
376 await fetchEmails();
377 toast.success(
378 __( 'Successfully restored.', 'presto-player' )
379 );
380 } catch ( error ) {
381 toast.error(
382 error?.message ||
383 __( 'Failed to restore.', 'presto-player' )
384 );
385 }
386 setOpenActionPopup( false );
387 },
388 cancelCallback: () => setOpenActionPopup( false ),
389 } );
390 setOpenActionPopup( true );
391 return;
392
393 case 'publish':
394 case 'draft': {
395 const isPublish = action === 'publish';
396 setActionPopupData( {
397 title: isPublish
398 ? __( 'Mark as Publish?', 'presto-player' )
399 : __( 'Save as Draft?', 'presto-player' ),
400 description: isPublish
401 ? __(
402 'Are you sure you want to mark this email submission as published?',
403 'presto-player'
404 )
405 : __(
406 'Are you sure you want to save this email submission as draft?',
407 'presto-player'
408 ),
409 confirmText: isPublish
410 ? __( 'Mark as Publish', 'presto-player' )
411 : __( 'Save as Draft', 'presto-player' ),
412 confirmCallback: async () => {
413 try {
414 await apiFetch( {
415 path: `/presto-player/v1/email-submissions/${ id }`,
416 method: 'PUT',
417 data: { status: isPublish ? 'publish' : 'draft' },
418 } );
419 await fetchEmails();
420 toast.success(
421 isPublish
422 ? __( 'Successfully published.', 'presto-player' )
423 : __( 'Successfully drafted.', 'presto-player' )
424 );
425 } catch ( error ) {
426 toast.error(
427 error?.message ||
428 ( isPublish
429 ? __( 'Failed to publish.', 'presto-player' )
430 : __( 'Failed to draft.', 'presto-player' ) )
431 );
432 }
433 setOpenActionPopup( false );
434 },
435 cancelCallback: () => setOpenActionPopup( false ),
436 } );
437 setOpenActionPopup( true );
438 }
439 }
440 };
441
442 const actionMenus = [
443 { value: 'draft', label: __( 'Save as Draft', 'presto-player' ), icon: <FolderArchive width="15" height="15" /> },
444 { value: 'publish', label: __( 'Mark as Publish', 'presto-player' ), icon: <CheckCheck width="15" height="15" /> },
445 { value: 'trash', label: __( 'Move to Trash', 'presto-player' ), icon: <Trash width="15" height="15" /> },
446 { value: 'restore', label: __( 'Restore', 'presto-player' ), icon: <ArchiveRestore width="15" height="15" /> },
447 { value: 'delete', label: __( 'Delete Permanently', 'presto-player' ), icon: <Trash2 width="15" height="15" /> },
448 ];
449
450 const renderActionMenu = ( item ) => {
451 const status = item?.status || 'publish';
452 const isTrashed = status === 'trash';
453 // Trashed items get only Restore + Delete; everything else gets the
454 // status-change actions plus Move to Trash, with the current status filtered out.
455 const visible = isTrashed
456 ? actionMenus.filter( ( a ) => a.value === 'restore' || a.value === 'delete' )
457 : actionMenus.filter(
458 ( a ) =>
459 a.value !== 'restore' &&
460 a.value !== 'delete' &&
461 a.value !== status
462 );
463 return visible.map( ( action ) => (
464 <DropdownMenu.Item
465 key={ action.value }
466 onClick={ () => handleMenuActions( item.id, action.value ) }
467 className="text-sm"
468 >
469 <div className="flex items-center gap-2">
470 { action.icon }
471 { action.label }
472 </div>
473 </DropdownMenu.Item>
474 ) );
475 };
476
477 const onNewEmailClick = ( e ) => {
478 e.preventDefault();
479 e.stopPropagation();
480 setSelectedEmailForEdit( null );
481 setShowEmailPopup( true );
482 };
483
484 const handleEmailFormSuccess = async ( data ) => {
485 if ( ! data || typeof data !== 'object' ) {
486 return;
487 }
488 const emailStr =
489 data.email && typeof data.email === 'string' ? data.email.trim() : '';
490 if ( ! emailStr ) {
491 return;
492 }
493 const isEdit = Boolean( data.id );
494 try {
495 const saved = await apiFetch( {
496 path: isEdit
497 ? `${ EMAIL_SUBMISSIONS_PATH }/${ data.id }`
498 : EMAIL_SUBMISSIONS_PATH,
499 method: isEdit ? 'PUT' : 'POST',
500 data: {
501 email: emailStr,
502 status: data.status || 'publish',
503 visibility: data.visibility || 'public',
504 date: data.date,
505 video_title: data.video_title || '',
506 preset_name: data.preset_name || '',
507 },
508 } );
509 // Local update mirrors MediaHub's handleSettingsSuccess. Refetching
510 // would flip `loading` to true and unmount the open dialog mid-save.
511 setRawEmails( ( prev ) =>
512 isEdit
513 ? prev.map( ( item ) =>
514 item.id === data.id ? { ...item, ...saved } : item
515 )
516 : [ saved, ...prev ]
517 );
518 if ( ! isEdit ) {
519 // Promote the dialog from Add to Edit so a second save updates
520 // the same submission instead of creating a duplicate.
521 setSelectedEmailForEdit( saved );
522 }
523 toast.success(
524 isEdit
525 ? __( 'Email updated.', 'presto-player' )
526 : __( 'Email added.', 'presto-player' )
527 );
528 } catch ( error ) {
529 toast.error(
530 error?.message || __( 'Failed to save.', 'presto-player' )
531 );
532 throw error;
533 }
534 };
535
536 const handleCheckboxChange = ( checked, value ) => {
537 setSelected( ( prev ) =>
538 checked ? [ ...prev, value.id ] : prev.filter( ( id ) => id !== value.id )
539 );
540 };
541
542 const filteredAndSortedEmails = useMemo( () => {
543 return ( emails || [] ).filter( ( item ) => {
544 const matchesSearch = searchTerm
545 ? ( item.email || '' )
546 .toLowerCase()
547 .includes( searchTerm.toLowerCase() )
548 : true;
549
550 let matchesMonth = true;
551 if ( filterMonth && item.date ) {
552 const itemDate = new Date( item.date );
553 if ( isNaN( itemDate.getTime() ) ) {
554 matchesMonth = false;
555 } else {
556 const yyyy = itemDate.getFullYear();
557 const mm = String( itemDate.getMonth() + 1 ).padStart( 2, '0' );
558 matchesMonth = `${ yyyy }-${ mm }` === filterMonth;
559 }
560 }
561
562 const itemStatus = item.status || 'publish';
563 const matchesStatus =
564 selectedStatus === 'all' || itemStatus === selectedStatus;
565
566 return matchesSearch && matchesMonth && matchesStatus;
567 } );
568 }, [ emails, searchTerm, filterMonth, selectedStatus ] );
569
570 const paginatedData = useMemo( () => {
571 const startIndex = ( currentPage - 1 ) * postCount;
572 return filteredAndSortedEmails.slice( startIndex, startIndex + postCount );
573 }, [ filteredAndSortedEmails, currentPage, postCount ] );
574
575 const toggleSelectAll = ( checked ) => {
576 setSelected( ( prev ) =>
577 togglePageSelection( prev, paginatedData, checked )
578 );
579 };
580
581 const containerClassName = 'p-8';
582
583 let content;
584 if ( loading ) {
585 content = <MediaHubPageSkeleton />;
586 } else if ( ! rawEmails?.length ) {
587 content = (
588 <Container
589 className={ `${ containerClassName } flex-1 items-center justify-start pt-16` }
590 direction="column"
591 >
592 <NoFound
593 icon={
594 <img
595 src={ emailsEmptyState }
596 width={ 70 }
597 height={ 53 }
598 alt=""
599 />
600 }
601 title={ __( 'Your email submissions will be displayed here.', 'presto-player' ) }
602 description={ __(
603 'Click "Add New Submission" to add a new email submission.',
604 'presto-player'
605 ) }
606 buttonText={ __( 'Add New Submission', 'presto-player' ) }
607 buttonIcon={ <Plus aria-label="icon" role="img" /> }
608 onButtonClick={ onNewEmailClick }
609 />
610 </Container>
611 );
612 } else {
613 content = (
614 <Container className={ containerClassName } gap="md" direction="column">
615 <PageHeader
616 title={ __( 'Emails', 'presto-player' ) }
617 showFilter={ showFilter }
618 setShowFilter={ setShowFilter }
619 showFilterWhen={ rawEmails?.length > 0 }
620 searchPlaceholder={ __( 'Search emails…', 'presto-player' ) }
621 searchValue={ searchTerm }
622 onSearchChange={ setSearchTerm }
623 searchInputRef={ searchInputRef }
624 primaryButtonText={ __( 'Add New Submission', 'presto-player' ) }
625 onPrimaryClick={ onNewEmailClick }
626 />
627
628 <Container gap="md" direction="column">
629 <BulkActions
630 selected={ selected }
631 selectedLabel={ sprintf(
632 /* translators: %d is the number of email submissions selected */
633 _n(
634 '%d email submission selected',
635 '%d email submissions selected',
636 selected.length,
637 'presto-player'
638 ),
639 selected.length
640 ) }
641 onStatusChange={ handleBulkStatusChange }
642 onTrash={ ( selectedIds ) => {
643 setActionPopupData( {
644 title: __( 'Move Selected to Trash?', 'presto-player' ),
645 description: sprintf(
646 /* translators: %d: number of items being moved to trash */
647 _n(
648 'Are you sure you want to move %d email submission to the trash?',
649 'Are you sure you want to move %d email submissions to the trash?',
650 selectedIds.length,
651 'presto-player'
652 ),
653 selectedIds.length
654 ),
655 confirmText: __( 'Move to Trash', 'presto-player' ),
656 destructive: true,
657 confirmCallback: () => {
658 handleBulkTrash( selectedIds );
659 setOpenActionPopup( false );
660 },
661 cancelCallback: () => setOpenActionPopup( false ),
662 } );
663 setOpenActionPopup( true );
664 } }
665 onDelete={ ( selectedIds ) => {
666 setActionPopupData( {
667 title: __(
668 'Delete Selected Items?',
669 'presto-player'
670 ),
671 description: sprintf(
672 /* translators: %d is the number of items to delete */
673 __(
674 'Are you sure you want to delete %d item(s)? This action cannot be undone.',
675 'presto-player'
676 ),
677 selectedIds.length
678 ),
679 confirmText: __( 'Delete', 'presto-player' ),
680 destructive: true,
681 confirmCallback: () => {
682 handleBulkDelete( selectedIds );
683 setOpenActionPopup( false );
684 },
685 cancelCallback: () =>
686 setOpenActionPopup( false ),
687 } );
688 setOpenActionPopup( true );
689 } }
690 onCancel={ handleBulkCancel }
691 />
692 { rawEmails?.length > 0 && showFilter && (
693 <Filters
694 postCount={ postCount }
695 setPostCount={ setPostCount }
696 perPageLabel={ __( 'Emails', 'presto-player' ) }
697 selects={ [
698 { options: statusOptions, value: selectedStatus, onChange: setSelectedStatus },
699 { options: monthFilterOptions, value: filterMonth, onChange: setFilterMonth },
700 ] }
701 onClear={ handleClearFilters }
702 />
703 ) }
704 <Table
705 paginatedData={ paginatedData }
706 selected={ selected }
707 filteredAndSortedEmailsLength={ filteredAndSortedEmails.length }
708 postCount={ postCount }
709 currentPage={ currentPage }
710 setCurrentPage={ setCurrentPage }
711 sortField={ sortField }
712 sortOrder={ sortOrder }
713 onSort={ handleSort }
714 onToggleSelectAll={ toggleSelectAll }
715 onCheckboxChange={ handleCheckboxChange }
716 onMenuAction={ handleMenuActions }
717 renderActionMenu={ renderActionMenu }
718 />
719 </Container>
720 </Container>
721 );
722 }
723
724 // Popups render in every branch so the dialog stays mounted across
725 // loading/empty/populated transitions — without this, switching branches
726 // unmounts EmailFormPopup mid-save (visible as a close-and-reopen flicker).
727 return (
728 <>
729 { content }
730 <EmailFormPopup
731 open={ showEmailPopup }
732 onClose={ () => {
733 setShowEmailPopup( false );
734 setSelectedEmailForEdit( null );
735 } }
736 initialData={ selectedEmailForEdit }
737 onSuccess={ handleEmailFormSuccess }
738 />
739 <ConfirmPopup
740 openConfirmPopup={ openActionPopup }
741 setOpenConfirmPopup={ setOpenActionPopup }
742 title={ actionPopupData?.title || '' }
743 description={ actionPopupData?.description || '' }
744 confirmText={ actionPopupData?.confirmText || '' }
745 cancelText={
746 actionPopupData?.cancelText ||
747 __( 'Cancel', 'presto-player' )
748 }
749 confirmCallback={ actionPopupData?.confirmCallback }
750 cancelCallback={ actionPopupData?.cancelCallback }
751 destructive={ actionPopupData?.destructive || false }
752 />
753 </>
754 );
755 };
756
757 const Emails = () => {
758 if ( ! window.prestoPlayer?.isPremium ) {
759 return (
760 <ProGateOverlay>
761 <MediaHubPageSkeleton />
762 </ProGateOverlay>
763 );
764 }
765
766 return <EmailsContent />;
767 };
768
769 export default Emails;
770