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 / MediaHub / MediaRow.js
presto-player / src / admin / dashboard / components / MediaHub Last commit date
test 1 month ago BulkActions.js 1 month ago MediaRow.js 1 month ago PostSettings.js 1 month ago SearchBar.js 1 month ago index.js 1 month ago
MediaRow.js
329 lines
1 import React, { useState } from "react";
2 import { __ } from "@wordpress/i18n";
3 import { addQueryArgs } from "@wordpress/url";
4 import {
5 Tooltip,
6 Button,
7 DropdownMenu,
8 Table,
9 Badge,
10 Text,
11 } from "@bsf/force-ui";
12 import { Eye, PencilLine, Ellipsis, Copy, Check, Settings } from "lucide-react";
13 import { decodeHTMLEntities } from "../../utils";
14 import { getViewportBoundary } from "../../utils/viewportBoundary";
15 import iconWhiteSvg from "../../../../../img/icon-white.svg";
16
17 // Statuses for which the front-end permalink would 404 (or worse — leak the
18 // slug). For these we either redirect via the WP preview URL or hide the
19 // View button entirely (trash).
20 const PREVIEW_STATUSES = ["draft", "pending", "future", "private"];
21
22 // Resolve the View button target for a given row, accounting for the post
23 // status. Returns null when there's no meaningful front-end URL (trash).
24 const resolveViewUrl = (item) => {
25 const status = item?.status;
26 if (status === "trash") {
27 return null;
28 }
29 const baseUrl = item?.link || `?p=${item?.id}`;
30 if (PREVIEW_STATUSES.includes(status)) {
31 return addQueryArgs(baseUrl, { preview: "true" });
32 }
33 return baseUrl;
34 };
35
36 const MediaRow = ({
37 item,
38 selected,
39 onChangeSelection,
40 onEditClick,
41 renderActionMenu,
42 getBadge,
43 formatPublishDate,
44 handleOpenSettings,
45 }) => {
46 const [imageError, setImageError] = useState(false);
47 const [copied, setCopied] = useState(false);
48
49 const handleCopyShortcode = async (e) => {
50 e.preventDefault();
51 e.stopPropagation();
52 if (!item?.shortcode) return;
53
54 const flagCopied = () => {
55 setCopied(true);
56 setTimeout(() => setCopied(false), 2000);
57 };
58
59 try {
60 if (navigator.clipboard?.writeText && window.isSecureContext) {
61 await navigator.clipboard.writeText(item.shortcode);
62 flagCopied();
63 return;
64 }
65 } catch (err) {
66 // fall through to legacy path
67 }
68
69 const textarea = document.createElement("textarea");
70 textarea.value = item.shortcode;
71 textarea.setAttribute("readonly", "");
72 textarea.style.position = "fixed";
73 textarea.style.opacity = "0";
74 document.body.appendChild(textarea);
75 textarea.select();
76 try {
77 if (document.execCommand("copy")) {
78 flagCopied();
79 }
80 } catch (err) {
81 console.error("Failed to copy shortcode:", err);
82 }
83 document.body.removeChild(textarea);
84 };
85
86 return (
87 <Table.Row
88 value={item}
89 selected={selected}
90 onChangeSelection={onChangeSelection}
91 data-id={item?.id}
92 >
93 <Table.Cell className="text-left" style={{ maxWidth: 0 }}>
94 <div className="flex items-center gap-3 overflow-hidden">
95 {item?.poster_image && !imageError ? (
96 <img
97 src={item.poster_image}
98 alt={item?.title || ""}
99 className="flex-shrink-0 object-cover w-[75px] h-auto aspect-video rounded-[2px]"
100 loading="lazy"
101 onError={() => {
102 setImageError(true);
103 }}
104 />
105 ) : (
106 <div
107 className="flex-shrink-0 flex items-center justify-center w-[75px] aspect-video box-border bg-[#d1d5db] rounded-[2px]"
108 >
109 <img
110 src={iconWhiteSvg}
111 alt=""
112 width="20"
113 height="auto"
114 className="h-auto"
115 />
116 </div>
117 )}
118 {(() => {
119 const decodedTitle = decodeHTMLEntities(
120 item?.title || __("Untitled", "presto-player")
121 );
122 const MAX_TITLE_CHARS = 60;
123 const isTruncated = decodedTitle.length > MAX_TITLE_CHARS;
124 return (
125 <span
126 className="block truncate cursor-pointer flex-1 min-w-0"
127 onClick={(e) => {
128 e.preventDefault();
129 e.stopPropagation();
130 if (onEditClick) {
131 onEditClick(e, item?.id);
132 }
133 }}
134 >
135 {isTruncated ? (
136 <>
137 {decodedTitle.slice(0, MAX_TITLE_CHARS - 1)}
138 <Tooltip placement="top" content={decodedTitle} arrow>
139 <span className="inline-block"></span>
140 </Tooltip>
141 </>
142 ) : (
143 decodedTitle
144 )}
145 </span>
146 );
147 })()}
148 </div>
149 </Table.Cell>
150 <Table.Cell className="text-left">
151 {getBadge(item?.status)}
152 </Table.Cell>
153 <Table.Cell className="text-left">
154 <div className="flex flex-wrap gap-x-1 gap-y-2">
155 {item?.tags && item.tags.length > 0 ? (
156 item.tags
157 .slice(0, 10)
158 .map((tag) => (
159 <Badge
160 key={tag.id}
161 variant="neutral"
162 label={decodeHTMLEntities(tag.name)}
163 size="sm"
164 className="text-xs"
165 />
166 ))
167 ) : (
168 <Text className="text-text-secondary text-sm">
169 {__("No tags", "presto-player")}
170 </Text>
171 )}
172 {item?.tags && item.tags.length > 10 && (
173 <Tooltip
174 content={item.tags
175 .slice(10)
176 .map((tag) => decodeHTMLEntities(tag.name))
177 .join(", ")}
178 arrow
179 placement="top"
180 >
181 <Badge
182 variant="neutral"
183 label={`+${item.tags.length - 10}`}
184 size="sm"
185 className="text-xs"
186 />
187 </Tooltip>
188 )}
189 </div>
190 </Table.Cell>
191 <Table.Cell className="whitespace-nowrap text-left">
192 <div className="flex items-center gap-2">
193 {item?.shortcode ? (
194 <code
195 className="font-mono text-xs px-3 py-2 rounded bg-field-secondary-background outline outline-1 outline-border-subtle text-text-primary cursor-text select-all"
196 onClick={(e) => e.stopPropagation()}
197 >
198 {item.shortcode}
199 </code>
200 ) : (
201 <span className="font-mono text-xs px-3 py-2 rounded bg-field-secondary-background outline outline-1 outline-border-subtle text-text-tertiary">
202 {__("No shortcode", "presto-player")}
203 </span>
204 )}
205 {item?.shortcode && (
206 <Tooltip
207 content={
208 copied
209 ? __("Copied!", "presto-player")
210 : __("Copy shortcode", "presto-player")
211 }
212 arrow
213 placement="top"
214 >
215 <Button
216 variant="ghost"
217 icon={
218 copied ? (
219 <Check width="14" height="14" />
220 ) : (
221 <Copy width="14" height="14" />
222 )
223 }
224 size="xs"
225 className={`flex-shrink-0 transition-colors ${
226 copied
227 ? "text-green-600 hover:text-green-700"
228 : "text-icon-secondary hover:text-icon-primary"
229 }`}
230 aria-label={__("Copy shortcode", "presto-player")}
231 onClick={handleCopyShortcode}
232 />
233 </Tooltip>
234 )}
235 </div>
236 </Table.Cell>
237 <Table.Cell className="text-left whitespace-nowrap">
238 {formatPublishDate(item?.post_date)}
239 </Table.Cell>
240 <Table.Cell className="text-right">
241 <div className="flex items-center justify-center gap-2">
242 {(() => {
243 const viewUrl = resolveViewUrl(item);
244 if (!viewUrl) {
245 return null;
246 }
247 const isPreview = PREVIEW_STATUSES.includes(item?.status);
248 const label = isPreview
249 ? __("Preview", "presto-player")
250 : __("View", "presto-player");
251 return (
252 <Tooltip content={label} arrow placement="top">
253 <Button
254 variant="ghost"
255 icon={<Eye />}
256 size="xs"
257 className="text-icon-secondary hover:text-icon-primary"
258 aria-label={label}
259 onClick={(e) => {
260 e.preventDefault();
261 e.stopPropagation();
262 window.open(viewUrl, "_blank", "noopener,noreferrer");
263 }}
264 />
265 </Tooltip>
266 );
267 })()}
268 <Tooltip content={__("Edit", "presto-player")} arrow placement="top">
269 <Button
270 variant="ghost"
271 icon={<PencilLine />}
272 size="xs"
273 className="text-icon-secondary hover:text-icon-primary"
274 aria-label={__("Edit", "presto-player")}
275 onClick={(e) => {
276 e.preventDefault();
277 e.stopPropagation();
278 if (onEditClick) {
279 onEditClick(e, item?.id);
280 }
281 }}
282 />
283 </Tooltip>
284 <Tooltip
285 content={__("Post Settings", "presto-player")}
286 arrow
287 placement="top"
288 >
289 <Button
290 variant="ghost"
291 size="xs"
292 icon={<Settings />}
293 aria-label={__("Post Settings", "presto-player")}
294 className="text-icon-secondary hover:text-icon-primary"
295 onClick={(e) => {
296 e.preventDefault();
297 e.stopPropagation();
298 if (handleOpenSettings) {
299 handleOpenSettings(e, item);
300 }
301 }}
302 />
303 </Tooltip>
304 <DropdownMenu boundary={getViewportBoundary()}>
305 <DropdownMenu.Trigger>
306 <Button
307 variant="ghost"
308 icon={<Ellipsis />}
309 size="xs"
310 className="text-icon-secondary hover:text-icon-primary z-0"
311 aria-label={__("More Options", "presto-player")}
312 />
313 </DropdownMenu.Trigger>
314 <DropdownMenu.ContentWrapper className="z-10">
315 <DropdownMenu.Content className="w-48">
316 <DropdownMenu.List>
317 {renderActionMenu ? renderActionMenu(item) : null}
318 </DropdownMenu.List>
319 </DropdownMenu.Content>
320 </DropdownMenu.ContentWrapper>
321 </DropdownMenu>
322 </div>
323 </Table.Cell>
324 </Table.Row>
325 );
326 };
327
328 export default MediaRow;
329