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 / RankedTable.js
presto-player / src / admin / dashboard / components Last commit date
Emails 1 month ago EngagementChart 1 month ago MediaHub 1 month ago Onboarding 1 month ago Popup 1 month ago Skeletons 2 weeks ago WhatsNew 1 month ago charts 1 month ago test 1 month ago AdminMenuSync.js 1 month ago ChartEmptyState.js 1 month ago ChooseDate.js 1 month ago ColorPicker.js 1 month ago ExtendPlugins.js 1 month ago Filters.js 1 month ago Link.js 1 month ago Navbar.js 2 weeks ago NoFound.js 1 month ago PageHeader.js 1 month ago PluginRecommendations.js 1 month ago PostScheduleField.js 1 month ago PrestoPlayerIcon.js 1 month ago ProGateOverlay.js 1 month ago QuickAccess.js 1 month ago RankedTable.js 1 month ago StatCard.js 1 month ago TopMedia.js 1 month ago TopPerformingMedia.js 1 month ago TopUsers.js 1 month ago TruncatedTitle.js 1 month ago UpgradeNotice.js 1 month ago UpgradeToPro.js 1 month ago VideoModal.js 1 month ago WelcomeBanner.js 1 month ago
RankedTable.js
225 lines
1 const { __ } = wp.i18n;
2 import { Container, Table, Pagination, Text } from "@bsf/force-ui";
3
4 /**
5 * Build the page list for the pager — first/last pinned, current ± siblings,
6 * ellipses for gaps. Bounded output keeps the pager inside the card on every
7 * page so the last page stays reachable however many pages there are.
8 *
9 * Returns at most `boundary*2 + siblings*2 + 3` entries (default = 7).
10 */
11 const buildPagerWindow = (currentPage, totalPages, siblings = 1, boundary = 1) => {
12 const totalSlots = boundary * 2 + siblings * 2 + 3;
13 if (totalPages <= totalSlots) {
14 return Array.from({ length: totalPages }, (_, i) => i + 1);
15 }
16
17 const leftSibling = Math.max(currentPage - siblings, boundary + 1);
18 const rightSibling = Math.min(currentPage + siblings, totalPages - boundary);
19 const showLeftEllipsis = leftSibling > boundary + 2;
20 const showRightEllipsis = rightSibling < totalPages - boundary - 1;
21
22 const range = (start, end) =>
23 Array.from({ length: end - start + 1 }, (_, i) => start + i);
24
25 const head = range(1, boundary);
26 const tail = range(totalPages - boundary + 1, totalPages);
27
28 if (!showLeftEllipsis && showRightEllipsis) {
29 return [...range(1, boundary + siblings * 2 + 2), "ellipsis-right", ...tail];
30 }
31 if (showLeftEllipsis && !showRightEllipsis) {
32 return [
33 ...head,
34 "ellipsis-left",
35 ...range(totalPages - boundary - siblings * 2 - 1, totalPages),
36 ];
37 }
38 return [
39 ...head,
40 "ellipsis-left",
41 ...range(leftSibling, rightSibling),
42 "ellipsis-right",
43 ...tail,
44 ];
45 };
46
47 /**
48 * Card + table + pagination chrome shared by Analytics ranked-list widgets
49 * (Top Media, Top Users, etc.).
50 *
51 * The shared component owns ONLY the chrome: card wrapper, header, table
52 * layout, and the pagination footer ("X–Y of N items" + numbered page
53 * buttons). Per-row content is supplied by `columns`; an optional trailing
54 * action cell is supplied by `rowAction`. Pagination is fully controlled
55 * — pass the metadata + `onPageChange` callback from the parent's data
56 * hook (server-side pagination) and the footer wires itself up.
57 *
58 * @param {Object} props
59 * @param {string} props.title Card header text.
60 * @param {Array} props.columns Column definitions:
61 * { key, header, headerClassName?, cellClassName?, render(row) }
62 * @param {Array} props.rows Current page of rows.
63 * @param {Function} [props.rowKey] (row) => key. Falls back to row.id.
64 * @param {Function} [props.onRowClick] (row) => void. Makes the row clickable.
65 * @param {Function} [props.rowAction] (row) => ReactNode. Renders trailing action cell.
66 * @param {Object} [props.pagination] { currentPage, totalItems, totalPages, perPage, onPageChange }.
67 * When omitted, no footer renders.
68 * @param {ReactNode} [props.emptyState] Rendered when `rows` is empty.
69 * @param {string} [props.className] Extra classes applied to the outer card Container.
70 */
71 const RankedTable = ({
72 title,
73 columns = [],
74 rows = [],
75 rowKey,
76 onRowClick,
77 rowAction,
78 pagination,
79 emptyState = null,
80 className = "",
81 }) => {
82 const hasPagination = Boolean(pagination);
83 const total = pagination?.totalItems ?? rows.length;
84 const totalPages = pagination?.totalPages ?? 0;
85 const currentPage = pagination?.currentPage ?? 1;
86 const perPage = pagination?.perPage ?? rows.length;
87 const startIndex = hasPagination ? (currentPage - 1) * perPage : 0;
88 const endIndex = hasPagination
89 ? Math.min(startIndex + rows.length, total)
90 : rows.length;
91
92 const goToPage = (page) => {
93 pagination?.onPageChange?.(page);
94 };
95
96 const renderPaginationItems = () => {
97 if (totalPages <= 1) return null;
98
99 const items = buildPagerWindow(currentPage, totalPages).map((entry) =>
100 typeof entry === "number" ? (
101 <Pagination.Item
102 key={entry}
103 isActive={entry === currentPage}
104 onClick={() => goToPage(entry)}
105 >
106 {entry}
107 </Pagination.Item>
108 ) : (
109 <Pagination.Ellipsis key={entry} />
110 )
111 );
112
113 return (
114 <>
115 <Pagination.Previous
116 onClick={() => goToPage(Math.max(currentPage - 1, 1))}
117 disabled={currentPage === 1}
118 />
119 {items}
120 <Pagination.Next
121 onClick={() => goToPage(Math.min(currentPage + 1, totalPages))}
122 disabled={currentPage === totalPages}
123 />
124 </>
125 );
126 };
127
128 const cardClasses = [
129 "overflow-hidden border-0.5 border-solid border-border-subtle rounded-xl shadow-sm bg-background-primary",
130 "h-full w-full",
131 className,
132 ]
133 .filter(Boolean)
134 .join(" ");
135
136 const getKey = (row, idx) => {
137 if (rowKey) return rowKey(row);
138 return row?.id ?? idx;
139 };
140
141 return (
142 <Container direction="column" gap="none" className={cardClasses}>
143 <Container align="center" gap="xs" className="px-3 py-3">
144 <Text className="text-base font-semibold text-text-primary">
145 {title}
146 </Text>
147 </Container>
148
149 {rows.length === 0 ? (
150 emptyState
151 ) : (
152 <>
153 <div className="[&>div]:rounded-none [&>div]:border-b-0">
154 <Table className="shadow-none border-0 rounded-none">
155 <Table.Head className="bg-background-primary border-b-0.5">
156 {columns.map((col, idx) => (
157 <Table.HeadCell
158 key={col.key ?? idx}
159 className={col.headerClassName}
160 >
161 {col.header}
162 </Table.HeadCell>
163 ))}
164 {rowAction ? <Table.HeadCell className="py-2" /> : null}
165 </Table.Head>
166
167 <Table.Body>
168 {rows.map((row, idx) => (
169 <Table.Row
170 key={getKey(row, idx)}
171 className={
172 onRowClick
173 ? "cursor-pointer hover:bg-background-secondary"
174 : undefined
175 }
176 onClick={onRowClick ? () => onRowClick(row) : undefined}
177 >
178 {columns.map((col, colIdx) => (
179 <Table.Cell
180 key={col.key ?? colIdx}
181 className={col.cellClassName}
182 >
183 {col.render(row)}
184 </Table.Cell>
185 ))}
186 {rowAction ? (
187 <Table.Cell className="whitespace-nowrap py-2">
188 {rowAction(row)}
189 </Table.Cell>
190 ) : null}
191 </Table.Row>
192 ))}
193 </Table.Body>
194 </Table>
195 </div>
196
197 {hasPagination && totalPages > 1 ? (
198 <div className="bg-background-primary px-4 py-3 border-t-[0.5px] border-x-0 border-b-0 border-solid border-border-subtle">
199 <Container align="center" justify="between" className="w-full">
200 <Text
201 size="sm"
202 weight="regular"
203 className="text-text-secondary"
204 >
205 {`${startIndex + 1}${endIndex} ${__(
206 "of",
207 "presto-player"
208 )} ${total} ${__("items", "presto-player")}`}
209 </Text>
210 <Pagination className="w-fit [&_li]:!my-0">
211 <Pagination.Content>
212 {renderPaginationItems()}
213 </Pagination.Content>
214 </Pagination>
215 </Container>
216 </div>
217 ) : null}
218 </>
219 )}
220 </Container>
221 );
222 };
223
224 export default RankedTable;
225