Utils.js
156 lines
| 1 | /** |
| 2 | * Shared utils and constants for Emails page. |
| 3 | * Used by Table; matches MediaHub pattern (formatPublishDate, getBadge, truncation). |
| 4 | */ |
| 5 | // eslint-disable-next-line import/no-extraneous-dependencies |
| 6 | import React from 'react'; |
| 7 | const { __ } = wp.i18n; |
| 8 | import { Badge, Tooltip } from '@bsf/force-ui'; |
| 9 | |
| 10 | export const TRUNCATE_LENGTH = 40; |
| 11 | |
| 12 | export const statusOptions = [ |
| 13 | { label: __( 'All Status', 'presto-player' ), value: 'all' }, |
| 14 | { label: __( 'Trashed', 'presto-player' ), value: 'trash' }, |
| 15 | { label: __( 'Published', 'presto-player' ), value: 'publish' }, |
| 16 | { label: __( 'Draft', 'presto-player' ), value: 'draft' }, |
| 17 | { label: __( 'Pending Review', 'presto-player' ), value: 'pending' }, |
| 18 | { label: __( 'Private', 'presto-player' ), value: 'private' }, |
| 19 | { label: __( 'Scheduled', 'presto-player' ), value: 'future' }, |
| 20 | ]; |
| 21 | |
| 22 | /** |
| 23 | * Truncate text with tooltip for full value (table cells). Returns "—" for empty. |
| 24 | * |
| 25 | * @param {string} text - Value to show. |
| 26 | * @param {number} maxLen - Max length before truncate (default TRUNCATE_LENGTH). |
| 27 | * @return {React.ReactNode} Truncated text with ellipsis tooltip, or full string, or em dash if empty. |
| 28 | */ |
| 29 | export function renderTruncated( text, maxLen = TRUNCATE_LENGTH ) { |
| 30 | const str = text || ''; |
| 31 | if ( ! str || str === '—' ) { |
| 32 | return '—'; |
| 33 | } |
| 34 | if ( str.length <= maxLen ) { |
| 35 | return str; |
| 36 | } |
| 37 | const tooltipContent = ( |
| 38 | <span |
| 39 | className="block max-w-[360px] break-all text-left" |
| 40 | style={ { whiteSpace: 'normal', wordBreak: 'break-all' } } |
| 41 | > |
| 42 | { str } |
| 43 | </span> |
| 44 | ); |
| 45 | return ( |
| 46 | <> |
| 47 | { str.slice( 0, maxLen - 1 ) } |
| 48 | <Tooltip content={ tooltipContent } arrow placement="top"> |
| 49 | <span className="inline-block">…</span> |
| 50 | </Tooltip> |
| 51 | </> |
| 52 | ); |
| 53 | } |
| 54 | |
| 55 | const statusBadgeConfig = { |
| 56 | publish: { label: __( 'Published', 'presto-player' ), variant: 'green' }, |
| 57 | draft: { label: __( 'Draft', 'presto-player' ), variant: 'yellow' }, |
| 58 | trash: { label: __( 'Trashed', 'presto-player' ), variant: 'red' }, |
| 59 | pending: { label: __( 'Pending Review', 'presto-player' ), variant: 'blue' }, |
| 60 | private: { label: __( 'Private', 'presto-player' ), variant: 'inverse' }, |
| 61 | future: { label: __( 'Scheduled', 'presto-player' ), variant: 'blue' }, |
| 62 | }; |
| 63 | |
| 64 | /** |
| 65 | * Status badge for table (publish, draft, pending, etc.). Matches MediaHub getBadge. |
| 66 | * |
| 67 | * @param {string} status - Post status. |
| 68 | * @return {React.ReactElement} Badge element for the status. |
| 69 | */ |
| 70 | export function getBadge( status ) { |
| 71 | const { label, variant } = statusBadgeConfig[ status ] || { |
| 72 | label: __( 'Unknown', 'presto-player' ), |
| 73 | variant: 'gray', |
| 74 | }; |
| 75 | return <Badge className="w-fit" variant={ variant } label={ label } />; |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Format date string for table display (YYYY/MM/DD at h:mm am/pm). "Just Now" for invalid. |
| 80 | * |
| 81 | * @param {string} dateString - ISO or date string. |
| 82 | * @return {string} Formatted date (YYYY/MM/DD at h:mm am/pm) or "Just Now" if invalid. |
| 83 | */ |
| 84 | export function formatPublishDate( dateString ) { |
| 85 | if ( ! dateString ) { |
| 86 | return __( 'Just Now', 'presto-player' ); |
| 87 | } |
| 88 | const date = new Date( dateString ); |
| 89 | if ( isNaN( date.getTime() ) ) { |
| 90 | return __( 'Just Now', 'presto-player' ); |
| 91 | } |
| 92 | const yyyy = date.getFullYear(); |
| 93 | const mm = String( date.getMonth() + 1 ).padStart( 2, '0' ); |
| 94 | const dd = String( date.getDate() ).padStart( 2, '0' ); |
| 95 | const hours = date.getHours(); |
| 96 | const minutes = String( date.getMinutes() ).padStart( 2, '0' ); |
| 97 | const ampm = hours >= 12 ? 'pm' : 'am'; |
| 98 | const hour12 = hours % 12 || 12; |
| 99 | return `${ yyyy }/${ mm }/${ dd } at ${ hour12 }:${ minutes } ${ ampm }`; |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Compute the next selection when the header bulk-select checkbox is |
| 104 | * toggled on a paginated table. Checking unions the current page's ids |
| 105 | * with the existing selection (so picks on other pages survive a page |
| 106 | * change); unchecking removes only the current page's ids. |
| 107 | * |
| 108 | * @param {Array<string|number>} prev - Currently selected ids. |
| 109 | * @param {Array<{ id: string|number }>} pageItems - Rows visible on the current page. |
| 110 | * @param {boolean} checked - true to add, false to remove. |
| 111 | * @return {Array<string|number>} The next selected-ids array. |
| 112 | */ |
| 113 | export function togglePageSelection( prev, pageItems, checked ) { |
| 114 | const prevIds = prev || []; |
| 115 | const pageIds = ( pageItems || [] ).map( ( item ) => item.id ); |
| 116 | if ( checked ) { |
| 117 | return Array.from( new Set( [ ...prevIds, ...pageIds ] ) ); |
| 118 | } |
| 119 | return prevIds.filter( ( id ) => ! pageIds.includes( id ) ); |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * Whether every row on the current page is in the selection. Empty page |
| 124 | * returns false so the header checkbox doesn't render checked when there |
| 125 | * is nothing on screen. |
| 126 | * |
| 127 | * @param {Array<{ id: string|number }>} pageItems |
| 128 | * @param {Array<string|number>} selected |
| 129 | * @return {boolean} |
| 130 | */ |
| 131 | export function isPageFullySelected( pageItems, selected ) { |
| 132 | if ( ! pageItems?.length ) { |
| 133 | return false; |
| 134 | } |
| 135 | const selectedIds = selected || []; |
| 136 | return pageItems.every( ( item ) => selectedIds.includes( item.id ) ); |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * Whether some — but not all — rows on the current page are in the |
| 141 | * selection. Drives the header checkbox's indeterminate state. |
| 142 | * |
| 143 | * @param {Array<{ id: string|number }>} pageItems |
| 144 | * @param {Array<string|number>} selected |
| 145 | * @return {boolean} |
| 146 | */ |
| 147 | export function isPagePartiallySelected( pageItems, selected ) { |
| 148 | if ( ! pageItems?.length ) { |
| 149 | return false; |
| 150 | } |
| 151 | const selectedIds = selected || []; |
| 152 | const any = pageItems.some( ( item ) => selectedIds.includes( item.id ) ); |
| 153 | const all = pageItems.every( ( item ) => selectedIds.includes( item.id ) ); |
| 154 | return any && ! all; |
| 155 | } |
| 156 |