PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
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 / components / Emails / Table.js
presto-player / src / admin / dashboard / components / Emails Last commit date
test 1 month ago Table.js 1 month ago Utils.js 1 month ago index.js 1 month ago
Table.js
339 lines
1 /**
2 * Data table: sortable columns, pagination, row actions. Matches MediaHub table pattern.
3 */
4 const { __ } = wp.i18n;
5 import {
6 Table as ForceTable,
7 Pagination,
8 Button,
9 Tooltip,
10 DropdownMenu,
11 Text,
12 } from '@bsf/force-ui';
13 import {
14 ChevronsUpDown,
15 Settings,
16 Trash,
17 Trash2,
18 ArchiveRestore,
19 Ellipsis,
20 Info,
21 } from 'lucide-react';
22 import {
23 getBadge,
24 formatPublishDate,
25 isPageFullySelected,
26 isPagePartiallySelected,
27 } from './Utils';
28 import { getViewportBoundary } from '../../utils/viewportBoundary';
29
30 const Table = ( {
31 paginatedData,
32 selected,
33 filteredAndSortedEmailsLength,
34 postCount,
35 currentPage,
36 setCurrentPage,
37 sortField,
38 sortOrder,
39 onSort,
40 onToggleSelectAll,
41 onCheckboxChange,
42 onMenuAction,
43 renderActionMenu,
44 } ) => {
45 const totalPages =
46 postCount <= 0 ? 0 : Math.ceil( filteredAndSortedEmailsLength / postCount );
47 const startIndex =
48 filteredAndSortedEmailsLength === 0
49 ? 0
50 : ( currentPage - 1 ) * postCount + 1;
51 const endIndex = Math.min(
52 currentPage * postCount,
53 filteredAndSortedEmailsLength
54 );
55
56 const renderRowActions = ( item ) => {
57 const isTrashed = ( item.status || 'publish' ) === 'trash';
58 const inlineActions = isTrashed
59 ? [
60 {
61 value: 'restore',
62 label: __( 'Restore', 'presto-player' ),
63 icon: <ArchiveRestore />,
64 },
65 {
66 value: 'delete',
67 label: __( 'Delete Permanently', 'presto-player' ),
68 icon: <Trash2 />,
69 },
70 ]
71 : [
72 {
73 value: 'edit',
74 label: __( 'Post Settings', 'presto-player' ),
75 icon: <Settings />,
76 },
77 {
78 value: 'trash',
79 label: __( 'Move to Trash', 'presto-player' ),
80 icon: <Trash />,
81 },
82 ];
83
84 return (
85 <>
86 { inlineActions.map( ( action ) => (
87 <Tooltip
88 key={ action.value }
89 content={ action.label }
90 arrow
91 placement="top"
92 >
93 <Button
94 variant="ghost"
95 icon={ action.icon }
96 size="xs"
97 className="text-icon-secondary hover:text-icon-primary"
98 aria-label={ action.label }
99 onClick={ ( e ) => {
100 e.preventDefault();
101 e.stopPropagation();
102 onMenuAction( item.id, action.value );
103 } }
104 />
105 </Tooltip>
106 ) ) }
107 <DropdownMenu boundary={ getViewportBoundary() }>
108 <DropdownMenu.Trigger>
109 <Button
110 variant="ghost"
111 icon={ <Ellipsis /> }
112 size="xs"
113 className="text-icon-secondary hover:text-icon-primary z-0"
114 aria-label={ __( 'More Options', 'presto-player' ) }
115 />
116 </DropdownMenu.Trigger>
117 <DropdownMenu.ContentWrapper className="z-10">
118 <DropdownMenu.Content className="w-48">
119 <DropdownMenu.List>
120 { renderActionMenu ? renderActionMenu( item ) : null }
121 </DropdownMenu.List>
122 </DropdownMenu.Content>
123 </DropdownMenu.ContentWrapper>
124 </DropdownMenu>
125 </>
126 );
127 };
128
129 const renderPagination = () => {
130 if ( totalPages <= 1 ) {
131 return null;
132 }
133
134 const pages = [];
135 const renderPageItem = ( i ) => (
136 <Pagination.Item
137 key={ i }
138 isActive={ i === currentPage }
139 onClick={ () => setCurrentPage( i ) }
140 >
141 { i }
142 </Pagination.Item>
143 );
144 const showEllipsis = ( key ) => <Pagination.Ellipsis key={ key } />;
145
146 pages.push( renderPageItem( 1 ) );
147 if ( currentPage > 3 ) {
148 pages.push( showEllipsis( 'left-ellipsis' ) );
149 }
150 for (
151 let i = Math.max( 2, currentPage - 1 );
152 i <= Math.min( totalPages - 1, currentPage + 1 );
153 i++
154 ) {
155 pages.push( renderPageItem( i ) );
156 }
157 if ( currentPage < totalPages - 2 ) {
158 pages.push( showEllipsis( 'right-ellipsis' ) );
159 }
160 if ( totalPages > 1 ) {
161 pages.push( renderPageItem( totalPages ) );
162 }
163
164 return (
165 <>
166 <Pagination.Previous
167 onClick={ () =>
168 setCurrentPage( ( prev ) => Math.max( prev - 1, 1 ) )
169 }
170 disabled={ currentPage === 1 }
171 />
172 { pages }
173 <Pagination.Next
174 onClick={ () =>
175 setCurrentPage( ( prev ) => Math.min( prev + 1, totalPages ) )
176 }
177 disabled={ currentPage === totalPages }
178 />
179 </>
180 );
181 };
182
183 return (
184 <div className="gap-0">
185 <ForceTable checkboxSelection={ true }>
186 <ForceTable.Head
187 selected={ isPageFullySelected( paginatedData, selected ) }
188 onChangeSelection={ onToggleSelectAll }
189 indeterminate={ isPagePartiallySelected(
190 paginatedData,
191 selected
192 ) }
193 className="bg-background-primary items-center"
194 >
195 <ForceTable.HeadCell className="text-text-secondary items-center">
196 { __( 'Email', 'presto-player' ) }
197 </ForceTable.HeadCell>
198 <ForceTable.HeadCell className="text-text-secondary items-center">
199 { __( 'Status', 'presto-player' ) }
200 </ForceTable.HeadCell>
201 <ForceTable.HeadCell className="text-text-secondary">
202 <span className="inline-flex items-center gap-1.5">
203 { __( 'Video', 'presto-player' ) }
204 <Tooltip
205 content={ __(
206 'The video the viewer was watching when they submitted their email.',
207 'presto-player'
208 ) }
209 arrow
210 placement="top"
211 >
212 <Info className="size-3.5 text-icon-secondary cursor-help shrink-0" />
213 </Tooltip>
214 </span>
215 </ForceTable.HeadCell>
216 <ForceTable.HeadCell className="text-text-secondary">
217 <span className="inline-flex items-center gap-1.5">
218 { __( 'Preset', 'presto-player' ) }
219 <Tooltip
220 content={ __(
221 'The player preset configured to collect this email submission.',
222 'presto-player'
223 ) }
224 arrow
225 placement="top"
226 >
227 <Info className="size-3.5 text-icon-secondary cursor-help shrink-0" />
228 </Tooltip>
229 </span>
230 </ForceTable.HeadCell>
231 <ForceTable.HeadCell
232 onClick={ () => onSort( 'date' ) }
233 className="cursor-pointer items-center gap-2 text-text-secondary"
234 >
235 { __( 'Date', 'presto-player' ) }
236 <ChevronsUpDown
237 width="15"
238 height="15"
239 className="text-icon-secondary align-middle ml-2"
240 style={
241 sortField === 'date' && sortOrder === 'asc'
242 ? { transform: 'rotate(180deg)' }
243 : {}
244 }
245 />
246 </ForceTable.HeadCell>
247 <ForceTable.HeadCell className="items-center justify-center">
248 <span className="sr-only">
249 { __( 'Actions', 'presto-player' ) }
250 </span>
251 </ForceTable.HeadCell>
252 </ForceTable.Head>
253
254 <ForceTable.Body>
255 { paginatedData && paginatedData.length > 0 ? (
256 paginatedData.map( ( item ) => (
257 <ForceTable.Row
258 key={ item.id }
259 value={ item }
260 selected={ selected.includes( item.id ) }
261 onChangeSelection={ onCheckboxChange }
262 >
263 <ForceTable.Cell className="min-w-[180px] max-w-[280px] text-left">
264 <Text
265 as="span"
266 size="sm"
267 className="text-text-primary block truncate"
268 >
269 { item.email }
270 </Text>
271 </ForceTable.Cell>
272 <ForceTable.Cell className="min-w-[100px] text-left">
273 { getBadge( item.status || 'publish' ) }
274 </ForceTable.Cell>
275 <ForceTable.Cell className="min-w-[140px] max-w-[220px] text-left">
276 <Text
277 as="span"
278 size="sm"
279 className={ `${ item.video_title ? 'text-text-primary' : 'text-text-secondary' } block truncate` }
280 >
281 { item.video_title || __( 'No video', 'presto-player' ) }
282 </Text>
283 </ForceTable.Cell>
284 <ForceTable.Cell className="min-w-[120px] max-w-[200px] text-left">
285 <Text
286 as="span"
287 size="sm"
288 className={ `${ item.preset_name ? 'text-text-primary' : 'text-text-secondary' } block truncate` }
289 >
290 { item.preset_name || __( 'No preset', 'presto-player' ) }
291 </Text>
292 </ForceTable.Cell>
293 <ForceTable.Cell className="w-[200px] text-left whitespace-nowrap">
294 { formatPublishDate( item.date ) }
295 </ForceTable.Cell>
296 <ForceTable.Cell className="w-[130px] text-right">
297 <div className="flex items-center justify-center gap-2">
298 { renderRowActions( item ) }
299 </div>
300 </ForceTable.Cell>
301 </ForceTable.Row>
302 ) )
303 ) : (
304 <tr>
305 <td
306 colSpan="7"
307 className="px-6 py-8 text-center text-text-secondary"
308 >
309 { __( 'No emails found.', 'presto-player' ) }
310 </td>
311 </tr>
312 ) }
313 </ForceTable.Body>
314
315 { paginatedData?.length > 0 && (
316 <ForceTable.Footer className="bg-background-primary">
317 <div className="flex items-center justify-between w-full">
318 <span className="text-sm font-normal leading-5 text-text-secondary">
319 { `${ startIndex }${ endIndex } ${ __(
320 'of',
321 'presto-player'
322 ) } ${ filteredAndSortedEmailsLength } ${ __(
323 'items',
324 'presto-player'
325 ) }` }
326 </span>
327 <Pagination className="w-fit">
328 <Pagination.Content>{ renderPagination() }</Pagination.Content>
329 </Pagination>
330 </div>
331 </ForceTable.Footer>
332 ) }
333 </ForceTable>
334 </div>
335 );
336 };
337
338 export default Table;
339