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 |