Skip to content

Commit b13dc8b

Browse files
authored
Merge pull request #1899 from rtCamp/develop
Develop to main sync for v1.11.0 release
2 parents 07e72ae + 3ee5e88 commit b13dc8b

19 files changed

Lines changed: 959 additions & 191 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog #
22

3+
## v1.11.0 (May 27, 2026) ##
4+
5+
- Tweak: Extended GoDAM analytics scripts to track Reel Pop interactions from GoDAM for Woo.
6+
- Tweak: Added a migration script to sync existing virtual media to GoDAM Central.
7+
- Fix: Improved video selection UX in the handpicked mode of the Video Gallery block.
8+
39
## v1.10.2 (May 22, 2026) ##
410

511
- Feat: Enhanced Type 1 video analytics tracking with viewport detection.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Tested up to: 7.0
1010

1111
Requires PHP: 7.4
1212

13-
Stable tag: 1.10.2
13+
Stable tag: 1.11.0
1414

1515
License: [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html)
1616

assets/src/blocks/godam-gallery-v2-item/edit.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import {
88
useBlockProps,
99
MediaUpload,
1010
MediaUploadCheck,
11+
store as blockEditorStore,
1112
} from '@wordpress/block-editor';
1213
import { Button } from '@wordpress/components';
13-
import { useDispatch, useSelect } from '@wordpress/data';
14+
import { useDispatch, useSelect, select as dataSelect } from '@wordpress/data';
1415
import { store as coreStore } from '@wordpress/core-data';
16+
import { store as noticesStore } from '@wordpress/notices';
1517
import { closeSmall, pencil, video as videoIcon } from '@wordpress/icons';
1618
import { useRef, useEffect } from '@wordpress/element';
1719

@@ -54,6 +56,7 @@ export default function Edit( { attributes, setAttributes, context, clientId } )
5456
const itemWidth = itemWidthMap[ itemWidthRaw ] || itemWidthMap.M;
5557
const viewRatio = context[ 'godam/galleryV2/viewRatio' ] || '16:9';
5658
const { removeBlock } = useDispatch( 'core/block-editor' );
59+
const { createNotice } = useDispatch( noticesStore );
5760

5861
// Ref to track the GoDAM virtual media ID from the most recent selection.
5962
const pendingVirtualMediaId = useRef( null );
@@ -65,9 +68,38 @@ export default function Edit( { attributes, setAttributes, context, clientId } )
6568
if ( ! mediaItem?.id ) {
6669
return;
6770
}
71+
// Only allow video attachments.
72+
if ( mediaItem.type && mediaItem.type !== 'video' ) {
73+
createNotice( 'warning', __( 'Only video files can be added to the gallery.', 'godam' ), {
74+
type: 'snackbar',
75+
isDismissible: true,
76+
} );
77+
return;
78+
}
79+
if ( mediaItem.mime && ! mediaItem.mime.startsWith( 'video/' ) ) {
80+
createNotice( 'warning', __( 'Only video files can be added to the gallery.', 'godam' ), {
81+
type: 'snackbar',
82+
isDismissible: true,
83+
} );
84+
return;
85+
}
6886
pendingVirtualMediaId.current = mediaItem.id;
6987
const numericId = parseInt( mediaItem.id, 10 );
7088
if ( numericId > 0 && String( numericId ) === String( mediaItem.id ) ) {
89+
// Prevent selecting a video already used by a sibling.
90+
const { getBlockRootClientId, getBlock } = dataSelect( blockEditorStore );
91+
const parentClientId = getBlockRootClientId( clientId );
92+
const parentBlock = getBlock( parentClientId );
93+
const isDuplicate = ( parentBlock?.innerBlocks || [] ).some(
94+
( block ) => block.clientId !== clientId && block.attributes?.videoId === numericId,
95+
);
96+
if ( isDuplicate ) {
97+
createNotice( 'warning', __( 'This video is already in the gallery.', 'godam' ), {
98+
type: 'snackbar',
99+
isDismissible: true,
100+
} );
101+
return;
102+
}
71103
setAttributes( { videoId: numericId } );
72104
}
73105
};
@@ -83,6 +115,22 @@ export default function Edit( { attributes, setAttributes, context, clientId } )
83115
String( pendingVirtualMediaId.current ) === String( virtualMediaId )
84116
) {
85117
pendingVirtualMediaId.current = null;
118+
119+
// Prevent selecting a video already used by a sibling.
120+
const { getBlockRootClientId, getBlock } = dataSelect( blockEditorStore );
121+
const parentClientId = getBlockRootClientId( clientId );
122+
const parentBlock = getBlock( parentClientId );
123+
const isDuplicate = ( parentBlock?.innerBlocks || [] ).some(
124+
( block ) => block.clientId !== clientId && block.attributes?.videoId === attachment.id,
125+
);
126+
if ( isDuplicate ) {
127+
createNotice( 'warning', __( 'This video is already in the gallery.', 'godam' ), {
128+
type: 'snackbar',
129+
isDismissible: true,
130+
} );
131+
return;
132+
}
133+
86134
setAttributes( { videoId: attachment.id } );
87135
}
88136
};
@@ -92,7 +140,7 @@ export default function Edit( { attributes, setAttributes, context, clientId } )
92140
return () => {
93141
document.removeEventListener( 'godam-virtual-attachment-created', handleVirtualAttachmentCreated );
94142
};
95-
}, [ setAttributes ] );
143+
}, [ clientId, setAttributes, createNotice ] );
96144

97145
const { media, hasResolvedMedia } = useSelect(
98146
( select ) => {

assets/src/blocks/godam-gallery-v2/edit.js

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ import {
2626
__experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon,
2727
Button,
2828
} from '@wordpress/components';
29-
import { useDispatch, useSelect } from '@wordpress/data';
29+
import { useDispatch, useSelect, select as dataSelect } from '@wordpress/data';
3030
import { store as coreStore } from '@wordpress/core-data';
31+
import { store as noticesStore } from '@wordpress/notices';
3132
import { useCallback, useEffect, useMemo, useRef, useState } from '@wordpress/element';
3233
import { columns, grid, listView, plus } from '@wordpress/icons';
3334

@@ -172,6 +173,7 @@ const AddVideoAppender = ( { onSelect } ) => (
172173
<MediaUploadCheck>
173174
<MediaUpload
174175
allowedTypes={ [ 'video' ] }
176+
multiple
175177
onSelect={ onSelect }
176178
render={ ( { open } ) => (
177179
<Button
@@ -214,7 +216,8 @@ export default function Edit( { attributes, setAttributes, clientId } ) {
214216
const [ startDatePopoverOpen, setStartDatePopoverOpen ] = useState( false );
215217
const [ endDatePopoverOpen, setEndDatePopoverOpen ] = useState( false );
216218
const [ dateError, setDateError ] = useState( '' );
217-
const { insertBlocks, updateBlockAttributes } = useDispatch( blockEditorStore );
219+
const { insertBlocks, updateBlockAttributes, removeBlock } = useDispatch( blockEditorStore );
220+
const { createNotice } = useDispatch( noticesStore );
218221

219222
// Tracks {virtualId, blockClientId} pairs for GoDAM virtual insertions
220223
// so the godam-virtual-attachment-created event can update the correct block.
@@ -367,28 +370,85 @@ export default function Edit( { attributes, setAttributes, clientId } ) {
367370
);
368371

369372
const insertHandpickedVideo = useCallback(
370-
( mediaItem ) => {
371-
if ( ! mediaItem?.id ) {
373+
( mediaItemOrArray ) => {
374+
const items = Array.isArray( mediaItemOrArray ) ? mediaItemOrArray : [ mediaItemOrArray ];
375+
if ( ! items.length ) {
372376
return;
373377
}
374378

375-
const numericId = parseInt( mediaItem.id, 10 );
376-
const isVirtual = ! ( numericId > 0 && String( numericId ) === String( mediaItem.id ) );
379+
// Get existing video IDs from inner blocks to prevent duplicates.
380+
const { getBlock } = dataSelect( blockEditorStore );
381+
const parentBlock = getBlock( clientId );
382+
const existingVideoIds = new Set(
383+
( parentBlock?.innerBlocks || [] )
384+
.map( ( block ) => block.attributes?.videoId )
385+
.filter( Boolean ),
386+
);
387+
388+
const newBlocks = [];
389+
let skippedNonVideo = false;
390+
let skippedDuplicate = false;
391+
392+
items.forEach( ( mediaItem ) => {
393+
if ( ! mediaItem?.id ) {
394+
return;
395+
}
396+
397+
// Only allow video attachments.
398+
if ( mediaItem.type && mediaItem.type !== 'video' ) {
399+
skippedNonVideo = true;
400+
return;
401+
}
402+
if ( mediaItem.mime && ! mediaItem.mime.startsWith( 'video/' ) ) {
403+
skippedNonVideo = true;
404+
return;
405+
}
406+
407+
const numericId = parseInt( mediaItem.id, 10 );
408+
const isVirtual = ! ( numericId > 0 && String( numericId ) === String( mediaItem.id ) );
409+
410+
if ( ! isVirtual ) {
411+
// Skip if this video is already in the gallery.
412+
if ( existingVideoIds.has( numericId ) ) {
413+
skippedDuplicate = true;
414+
return;
415+
}
416+
existingVideoIds.add( numericId );
417+
}
418+
419+
const newBlock = createBlock( 'godam/gallery-v2-item', {
420+
videoId: isVirtual ? 0 : numericId,
421+
} );
422+
423+
if ( isVirtual ) {
424+
pendingVirtualInserts.current.push( {
425+
virtualId: mediaItem.id,
426+
blockClientId: newBlock.clientId,
427+
} );
428+
}
377429

378-
const newBlock = createBlock( 'godam/gallery-v2-item', {
379-
videoId: isVirtual ? 0 : numericId,
430+
newBlocks.push( newBlock );
380431
} );
381432

382-
if ( isVirtual ) {
383-
pendingVirtualInserts.current.push( {
384-
virtualId: mediaItem.id,
385-
blockClientId: newBlock.clientId,
433+
if ( skippedNonVideo ) {
434+
createNotice( 'warning', __( 'Only video files can be added to the gallery.', 'godam' ), {
435+
type: 'snackbar',
436+
isDismissible: true,
437+
} );
438+
}
439+
440+
if ( skippedDuplicate ) {
441+
createNotice( 'warning', __( 'Duplicate videos were skipped.', 'godam' ), {
442+
type: 'snackbar',
443+
isDismissible: true,
386444
} );
387445
}
388446

389-
insertBlocks( newBlock, undefined, clientId );
447+
if ( newBlocks.length > 0 ) {
448+
insertBlocks( newBlocks, undefined, clientId );
449+
}
390450
},
391-
[ clientId, insertBlocks ],
451+
[ clientId, createNotice, insertBlocks ],
392452
);
393453

394454
// When GoDAM creates a real WP attachment, find the pending child block
@@ -409,6 +469,22 @@ export default function Edit( { attributes, setAttributes, clientId } ) {
409469
}
410470

411471
const [ { blockClientId } ] = pendingVirtualInserts.current.splice( idx, 1 );
472+
473+
// Skip if this video already exists in another gallery item.
474+
const { getBlock } = dataSelect( blockEditorStore );
475+
const parentBlock = getBlock( clientId );
476+
const isDuplicate = ( parentBlock?.innerBlocks || [] ).some(
477+
( block ) => block.clientId !== blockClientId && block.attributes?.videoId === attachment.id,
478+
);
479+
if ( isDuplicate ) {
480+
createNotice( 'warning', __( 'Duplicate videos were skipped.', 'godam' ), {
481+
type: 'snackbar',
482+
isDismissible: true,
483+
} );
484+
removeBlock( blockClientId );
485+
return;
486+
}
487+
412488
updateBlockAttributes( blockClientId, { videoId: attachment.id } );
413489
};
414490

@@ -417,7 +493,7 @@ export default function Edit( { attributes, setAttributes, clientId } ) {
417493
return () => {
418494
document.removeEventListener( 'godam-virtual-attachment-created', handleVirtualAttachmentCreated );
419495
};
420-
}, [ updateBlockAttributes ] );
496+
}, [ clientId, createNotice, removeBlock, updateBlockAttributes ] );
421497

422498
const renderVideoAppender = useCallback(
423499
() => <AddVideoAppender onSelect={ insertHandpickedVideo } />,

assets/src/blocks/godam-gallery-v2/editor.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
.block-list-appender {
2525
position: initial;
26+
width: 100%;
2627
}
2728
}
2829

assets/src/js/godam-player/analytics-helpers.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,50 @@
66
* is the single source of truth for the request schema.
77
*/
88

9+
/**
10+
* External dependencies
11+
*/
12+
import { v4 as uuidv4 } from 'uuid';
13+
14+
// Module-scope cache. Generated once on first event, reused for the rest of
15+
// the page load. Every type=1/2/3/4 event from the same page load shares this
16+
// UUID so the microservice can dedup with uniqExact(page_load_session_id).
17+
let _pageLoadSessionId = null;
18+
19+
/**
20+
* Return a stable UUID v4 for this page load.
21+
*
22+
* @return {string} UUID v4 (36 chars).
23+
*/
24+
export function getPageLoadSessionId() {
25+
if ( ! _pageLoadSessionId ) {
26+
_pageLoadSessionId = uuidv4();
27+
}
28+
return _pageLoadSessionId;
29+
}
30+
31+
/**
32+
* Normalized browser name (e.g. "Google Chrome", "Apple Safari").
33+
*
34+
* Wraps getUserAgent so callers outside this module can produce the same
35+
* config_browser_name string buildAnalyticsRequestBody emits — guarantees
36+
* type=1/2/3 and reel-pop type=4 device buckets line up.
37+
*
38+
* @return {string} Browser name.
39+
*/
40+
export function getBrowserName() {
41+
return getUserAgent( window.navigator.userAgent ).name;
42+
}
43+
44+
/**
45+
* Normalized OS / platform name (e.g. "Macintosh", "Windows", "Linux").
46+
*
47+
* @return {string} OS / platform name.
48+
*/
49+
export function getOSName() {
50+
return getUserAgent( window.navigator.userAgent ).platform;
51+
}
52+
953
/**
1054
* Check if we should skip analytics tracking.
1155
*
@@ -111,6 +155,7 @@ export function getUserAgent( userAgent ) {
111155
* @param {Array} [opts.videoIds] Array of [videoId, jobId] pairs (type 1).
112156
* @param {Array} [opts.ranges] Played time-range pairs (type 2).
113157
* @param {number} [opts.videoLength] Duration in seconds (type 2).
158+
* @param {number} [opts.reelPopId] Reel Pop CPT post ID (when event originates from a reel-pop modal).
114159
* @return {{ endpoint: string|null, body: Object|null }} Object with `endpoint` (the base
115160
* API URL) and `body` (the request payload). Both are `null` when the plugin token is
116161
* missing or unverified — callers must check `endpoint` before sending.
@@ -124,6 +169,7 @@ export function buildAnalyticsRequestBody( {
124169
videoIds = [],
125170
ranges = [],
126171
videoLength = 0,
172+
reelPopId = 0,
127173
} ) {
128174
const {
129175
endpoint,
@@ -194,6 +240,7 @@ export function buildAnalyticsRequestBody( {
194240
video_ids: type === 1 ? videoIds : [],
195241
ranges,
196242
video_length: videoLength || 0,
243+
page_load_session_id: getPageLoadSessionId(),
197244
};
198245

199246
// Only include job_id when it has a value — an empty string triggers
@@ -202,5 +249,19 @@ export function buildAnalyticsRequestBody( {
202249
body.job_id = jobId;
203250
}
204251

252+
const reelPopIdInt = parseInt( reelPopId, 10 );
253+
if ( reelPopIdInt > 0 ) {
254+
body.reel_pop_id = reelPopIdInt;
255+
}
256+
205257
return { endpoint, body };
206258
}
259+
260+
// Expose helpers on a global namespace so sibling plugins (e.g. godam-for-woo)
261+
// that are bundled separately can reuse them without a shared import path.
262+
// Reusing these guarantees parity between type=1/2/3 and type=4 envelopes.
263+
window.GoDAM = window.GoDAM || {};
264+
window.GoDAM.getPageLoadSessionId = getPageLoadSessionId;
265+
window.GoDAM.getBrowserName = getBrowserName;
266+
window.GoDAM.getOSName = getOSName;
267+
window.GoDAM.shouldSkipAnalytics = shouldSkipAnalytics;

0 commit comments

Comments
 (0)