diff --git a/app/assets/javascripts/add-playlist-option.js.coffee b/app/assets/javascripts/add-playlist-option.js.coffee index 7c2cfc3fb2..3f4260ece7 100644 --- a/app/assets/javascripts/add-playlist-option.js.coffee +++ b/app/assets/javascripts/add-playlist-option.js.coffee @@ -1,108 +1,138 @@ -addnew = 'Add new playlist' -select_element = $('#post_playlist_id') -add_success = false +# Copyright 2011-2023, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- -select_element.prepend(new Option(addnew)) +# This script will enable support for the html5 form attribute +# This should only be needed for IE but is currently applied wholesale +# to all disjointed submit elements as is needed in some of the workflow steps -getSearchTerm = () -> - return $('span.select2-search--dropdown input').val() +$ -> + add_new_playlist_option() -matchWithNew = (params, data) -> - term = params.term || '' - term = term.toLowerCase() - text = data.text.toLowerCase() - if (text.includes(addnew.toLowerCase()) || text.includes(term)) - return data - return null +@add_new_playlist_option = () -> + addnew = 'Add new playlist' + select_element = $('#post_playlist_id') + select_options = $('#post_playlist_id > option') + add_success = false + has_new_opt = false -sortWithNew = (data) -> - return data.sort((a, b) -> - if (b.text.trim() == addnew) - return 1 - if (a.text < b.text || a.text.trim() == addnew) - return -1 - if (a.text > b.text) - return 1 - return 0) + # Prepend 'Add new playlist' option only when not present + select_options.each -> + if @value == addnew + has_new_opt = true + + if !has_new_opt + select_element.prepend(new Option(addnew)) -formatAddNew = (data) -> - term = getSearchTerm() || '' - if (data.text == addnew ) - if (term != '') - term = addnew + ' "' + term + '"' - else - term = addnew - return ' - '+term+' - ' - else - start = data.text.toLowerCase().indexOf(term.toLowerCase()) - if (start != -1) - end = start+term.length-1 - return data.text.substring(0,start) + '' + data.text.substring(start, end+1) + '' + data.text.substring(end+1) - return data.text + getSearchTerm = () -> + return $('span.select2-search--dropdown input').val() -showNewPlaylistModal = (playlistName) -> - #set to defaults first - $('#new_playlist_submit').val('Create') - $('#new_playlist_submit').prop("disabled", false) - $('#playlist_title').val(playlistName) - $('#playlist_comment').val("") - $('#playlist_visibility_private').prop('checked', true) - #remove any possible old errors - $('#title_error').remove() - $('#playlist_title').parent().removeClass('has-error') - add_success = false - #finally show - $('#add-playlist-modal').modal('show') - return true + matchWithNew = (params, data) -> + term = params.term || '' + term = term.toLowerCase() + text = data.text.toLowerCase() + if (text.includes(addnew.toLowerCase()) || text.includes(term)) + return data + return null + + sortWithNew = (data) -> + return data.sort((a, b) -> + if (b.text.trim() == addnew) + return 1 + if (a.text < b.text || a.text.trim() == addnew) + return -1 + if (a.text > b.text) + return 1 + return 0) -$('#add-playlist-modal').on('hidden.bs.modal', () -> - if (!add_success) - op = select_element.children()[1] - if (op) - newval = op.value + formatAddNew = (data) -> + term = getSearchTerm() || '' + if (data.text == addnew ) + if (term != '') + term = addnew + ' "' + term + '"' + else + term = addnew + return ' + '+term+' + ' else - newval = -1 - select_element.val(newval).trigger('change.select2') -) + start = data.text.toLowerCase().indexOf(term.toLowerCase()) + if (start != -1) + end = start+term.length-1 + return data.text.substring(0,start) + '' + data.text.substring(start, end+1) + '' + data.text.substring(end+1) + return data.text + + select_element.select2({ + templateResult: formatAddNew + escapeMarkup: + (markup) -> return markup + matcher: matchWithNew + sorter: sortWithNew + }) + .on('select2:selecting', + (evt) -> + choice = evt.params.args.data.text + if (choice.indexOf(addnew) != -1) + showNewPlaylistModal(getSearchTerm()) + ) -select_element.select2({ - templateResult: formatAddNew - escapeMarkup: - (markup) -> return markup - matcher: matchWithNew - sorter: sortWithNew - }) - .on('select2:selecting', - (evt) -> - choice = evt.params.args.data.text - if (choice.indexOf(addnew) != -1) - showNewPlaylistModal(getSearchTerm()) - ) + showNewPlaylistModal = (playlistName) -> + #set to defaults first + $('#new_playlist_submit').val('Create') + $('#new_playlist_submit').prop("disabled", false) + $('#playlist_title').val(playlistName) + $('#playlist_comment').val("") + $('#playlist_visibility_private').prop('checked', true) + #remove any possible old errors + $('#title_error').remove() + $('#playlist_title').parent().removeClass('has-error') + add_success = false + #finally show + $('#add-playlist-modal').modal('show') + return true -$('#playlist_form').submit( - () -> - if($('#playlist_title').val()) - $('#new_playlist_submit').val('Saving...') - $('#new_playlist_submit').prop("disabled", true) - return true - if($('#title_error').length == 0) - $('#playlist_title') - .after('
Name is required
') - $('#playlist_title').parent().addClass('has-error') - return false -) + $('#add-playlist-modal').on('hidden.bs.modal', () -> + if (!add_success) + op = select_element.children()[1] + if (op) + newval = op.value + else + newval = -1 + select_element.val(newval).trigger('change.select2') + ) -$("#playlist_form").bind('ajax:success', - (event) -> - [data, status, xhr] = event.detail - $('#add-playlist-modal').modal('hide') - if (data.errors) - console.log(data.errors.title[0]) - else - add_success = true - select_element - .append(new Option(data.title, data.id.toString(), true, true)) - .trigger('change') + $('#playlist_form').submit( + () -> + if($('#playlist_title').val()) + $('#new_playlist_submit').val('Saving...') + $('#new_playlist_submit').prop("disabled", true) + return true + if($('#title_error').length == 0) + $('#playlist_title') + .after('
Name is required
') + $('#playlist_title').parent().addClass('has-error') + return false ) + + $("#playlist_form").bind('ajax:success', + (event) -> + [data, status, xhr] = event.detail + $('#add-playlist-modal').modal('hide') + if (data.errors) + console.log(data.errors.title[0]) + else + add_success = true + select_element + .append(new Option(data.title, data.id.toString(), true, true)) + .trigger('change') + ) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index d6afe47726..0a0738b589 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -52,8 +52,6 @@ //= stub mediaelement/plugins/markers //= stub mediaelement/plugins/quality-avalon //= stub mediaelement/plugins/quality-i18n -//= stub media_player_wrapper/mejs4_plugin_add_to_playlist -//= stub media_player_wrapper/mejs4_plugin_add_marker_to_playlist //= stub media_player_wrapper/mejs4_plugin_track_scrubber //= stub media_player_wrapper/mejs4_link_back //= stub media_player_wrapper/mejs4_plugin_playlist_items diff --git a/app/assets/javascripts/media_player_wrapper/mejs4_plugin_add_to_playlist.es6 b/app/assets/javascripts/media_player_wrapper/mejs4_plugin_add_to_playlist.es6 deleted file mode 100644 index ddce84108f..0000000000 --- a/app/assets/javascripts/media_player_wrapper/mejs4_plugin_add_to_playlist.es6 +++ /dev/null @@ -1,422 +0,0 @@ -// Copyright 2011-2022, The Trustees of Indiana University and Northwestern -// University. Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software distributed -// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, either express or implied. See the License for the -// specific language governing permissions and limitations under the License. - -'use strict'; - -/** - * Add To Playlist plugin - * - * A custom Avalon MediaElement 4 plugin for adding to a playlist - */ - -// Feature configuration -Object.assign(mejs.MepDefaults, { - // Any variable that can be configured by the end user belongs here. - // Make sure is unique by checking API and Configuration file. - // Add comments about the nature of each of these variables. -}); - -Object.assign(MediaElementPlayer.prototype, { - // Public variables (also documented according to JSDoc specifications) - - /** - * Feature constructor. - * - * Always has to be prefixed with `build` and the name that will be used in MepDefaults.features list - * @param {MediaElementPlayer} player - * @param {HTMLElement} controls - * @param {HTMLElement} layers - * @param {HTMLElement} media - */ - buildaddToPlaylist(player, controls, layers, media) { - const t = this; - const addTitle = 'Add to Playlist'; - let addToPlayListObj = t.addToPlayListObj; - - addToPlayListObj.player = player; - - addToPlayListObj.hasPlaylists = - addToPlayListObj.playlistEl && - addToPlayListObj.playlistEl.dataset.hasPlaylists === 'true'; - - addToPlayListObj.isVideo = player.isVideo; - - player.cleanaddToPlaylist(player); - - // Create plugin control button for player - player.addPlaylistButton = document.createElement('div'); - player.addPlaylistButton.className = - t.options.classPrefix + - 'button ' + - t.options.classPrefix + - 'add-to-playlist-button'; - player.addPlaylistButton.innerHTML = ``; - - let playlistBtn = player.addPlaylistButton.childNodes[0]; - - let enableBtn = () => { - if(player.duration > 0) { - playlistBtn.style.opacity = 1; - playlistBtn.style.cursor = 'pointer'; - playlistBtn.disabled = false; - clearInterval(timeCheck); - } - } - - // Enable add to playlist button after derivative is loaded - let timeCheck = setInterval(enableBtn, 500); - - // Add control button to player - t.addControlElement(player.addPlaylistButton, 'addToPlaylist'); - - // Set up click listener for the control button - player.addPlaylistButton.addEventListener( - 'click', - addToPlayListObj.handleControlClick.bind(t) - ); - - // Set click listeners for form elements - addToPlayListObj.bindHandleAdd = addToPlayListObj.handleAddClick.bind( - addToPlayListObj - ); - addToPlayListObj.bindHandleCancel = addToPlayListObj.handleCancelClick.bind( - addToPlayListObj - ); - addToPlayListObj.addFormClickListeners(); - - // Set up click listener for Sections - $('#accordion').on( - 'click', - addToPlayListObj.handleSectionLinkClick.bind(t) - ); - }, - - // Optionally, each feature can be destroyed setting a `clean` method - - /** - * Feature destructor. - * - * Always has to be prefixed with `clean` and the name that was used in MepDefaults.features list - * @param {MediaElementPlayer} player - * @param {HTMLElement} controls - * @param {HTMLElement} layers - * @param {HTMLElement} media - */ - cleanaddToPlaylist(player, controls, layers, media) { - const t = this; - let addToPlayListObj = t.addToPlayListObj; - - // Remove the click listener on accordion, which captures all section link clicks - $('#accordion').off('click'); - - $(addToPlayListObj.alertEl).hide(); - $(addToPlayListObj.playlistEl).hide(); - addToPlayListObj.resetForm.apply(addToPlayListObj); - - // Remove Add / Cancel button event listeners - if (addToPlayListObj.addButton !== null) { - addToPlayListObj.addButton.removeEventListener( - 'click', - addToPlayListObj.bindHandleAdd - ); - } - if (addToPlayListObj.cancelButton !== null) { - addToPlayListObj.cancelButton.removeEventListener( - 'click', - addToPlayListObj.bindHandleCancel - ); - } - if (player && player.addPlaylistButton) { - player.addPlaylistButton.remove(); - } - }, - - // Other optional public methods (all documented according to JSDoc specifications) - - /** - * The 'addToPlayListObj' object acts as a namespacer for all Add to Playlist - * specific variables and methods, so as not to pollute global or MEJS scope. - * @type {Object} - */ - addToPlayListObj: { - active: false, - addButton: document.getElementById('add_playlist_item_submit'), - alertEl: document.getElementById('add_to_playlist_alert'), - bindHandleAdd: null, - bindHandleCancel: null, - cancelButton: document.getElementById('add_playlist_item_cancel'), - formInputs: { - description: document.getElementById('playlist_item_description'), - end: document.getElementById('playlist_item_end'), - playlist: document.getElementById('post_playlist_id'), - start: document.getElementById('playlist_item_start'), - title: document.getElementById('playlist_item_title') - }, - hasPlaylists: false, - hasSections: $('#accordion').length > 0, - isVideo: null, - playlistEl: document.getElementById('add_to_playlist'), - - /** - * Add click listeners to the Add Playlists form buttons - * @function addFormClickListeners - * @return {void} - */ - addFormClickListeners: function() { - const t = this; - - if (t.hasPlaylists) { - t.addButton.addEventListener('click', t.bindHandleAdd); - t.cancelButton.addEventListener('click', t.bindHandleCancel); - } - }, - - /** - * Create a default add to playlist title from @currentStreamInfo - * Note there is some massaging of the data to get it into place based on whether - * sections and structural metadata exist. Perhaps server side could pre-parse the - * default title to account for scenarios in the future. - * @function createDefaultPlaylistTitle - * @return {string} defaultTitle - */ - createDefaultPlaylistTitle: function() { - const t = this; - let addToPlayListObj = t.addToPlayListObj; - let defaultTitle = - addToPlayListObj.player.options.playlistItemDefaultTitle; - - const currentStream = $('#accordion li a.current-stream'); - - if (currentStream.length > 0) { - let $firstCurrentStream = $(currentStream[0]); - let re1 = /^\s*\d\.\s*/; // index number in front of section title '1. ' - let re2 = /\s*\(.*\)$/; // duration notation at end of section title ' (2:00)' - let structureTitle = $firstCurrentStream - .text() - .replace(re1, '') - .replace(re2, '') - .trim(); - let parent = $firstCurrentStream - .closest('ul') - .closest('li') - .prev(); - - while (parent.length > 0) { - structureTitle = parent.text().trim() + ' - ' + structureTitle; - parent = parent - .closest('ul') - .closest('li') - .prev(); - } - defaultTitle = defaultTitle + ' - ' + structureTitle; - } - - return defaultTitle; - }, - - /** - * Checks whether the form elements which make up the Add To Playlist form are - * present in the DOM. If no playlists have been created, the values will be 'null'. - * If playlists have been created, then the values will be DOM element references. - * @function formHasDefinedInputs - * @return {Boolean} Does the Add to Playlist form have DOM element inputs? - */ - formHasDefinedInputs: function() { - let hasInputs = true; - const formInputs = this.formInputs; - - for (let prop in formInputs) { - if (!formInputs[prop]) { - hasInputs = false; - } - } - return hasInputs; - }, - - /** - * Handle the 'Add' button click; post form data via ajax and handle response - * @function handleAddClick - * @param {MouseEvent} e Event generated when Cancel form button clicked - * @return {void} - */ - handleAddClick: function(e) { - const t = this; - const p = $('#post_playlist_id').val(); - - $.ajax({ - url: '/playlists/' + p + '/items', - type: 'POST', - data: { - playlist_item: { - master_file_id: mejs4AvalonPlayer.currentStreamInfo.id, - title: $('#playlist_item_title').val(), - comment: $('#playlist_item_description').val(), - start_time: $('#playlist_item_start').val(), - end_time: $('#playlist_item_end').val() - } - } - }) - .done(t.handleAddClickSuccess.bind(t)) - .fail(t.handleAddClickError.bind(t)); - }, - - /** - * Add to playlist AJAX error handler - * @function handleAddClickError - * @param {Object} error AJAX response - * @return {void} - */ - handleAddClickError: function(error) { - const t = this; - let alertEl = t.alertEl; - let message = error.statusText || 'There was an error adding to playlist'; - if (error.responseJSON && error.responseJSON.message) { - message = error.responseJSON.message.join('
'); - } - alertEl.classList.remove('alert-success'); - alertEl.classList.add('alert-danger'); - alertEl.classList.add('add_to_playlist_alert_error'); - alertEl.querySelector('p').innerHTML = 'ERROR: ' + message; - $(alertEl).slideDown(); - }, - - /** - * Add to playlist AJAX success handler - * @function handleAddClickSuccess - * @param {Object} response AJAX response - * @return {void} - */ - handleAddClickSuccess: function(response) { - const t = this; - let alertEl = t.alertEl; - - alertEl.classList.remove('alert-danger'); - alertEl.classList.add('alert-success'); - alertEl.querySelector('p').innerHTML = response.message; - $(alertEl).slideDown(); - $(t.playlistEl).slideUp(); - t.resetForm(); - }, - - /** - * Handle cancel button click; hide form and alert windows. - * @function handleCancelClick - * @param {MouseEvent} e Event generated when Cancel form button clicked - * @return {void} - */ - handleCancelClick: function(e) { - const t = this; - - $(t.alertEl).slideUp(); - $(t.playlistEl).slideUp(); - t.resetForm(); - }, - - /** - * Handle control button click to toggle Add Playlist display - * @function handleControlClick - * @param {MouseEvent} e Event generated when Add to Playlist control button clicked - * @return {void} - */ - handleControlClick: function(e) { - const t = this; - let addToPlayListObj = t.addToPlayListObj; - if (addToPlayListObj.player.isFullScreen) { - addToPlayListObj.player.exitFullScreen(); - } - if (!addToPlayListObj.active) { - // Close any open alert displays - $(t.addToPlayListObj.alertEl).slideUp(); - - if (addToPlayListObj.hasPlaylists) { - // Load default values into form fields - t.addToPlayListObj.populateFormValues.apply(this); - } - } - // Toggle form display - $(t.addToPlayListObj.playlistEl).slideToggle(); - // Update active (is showing) state - t.addToPlayListObj.active = !t.addToPlayListObj.active; - }, - - /** - * Handle click events on the Sections and structural metadata links. - * @function handleSectionLinkClick - * @param {MouseEvent} e - * @return {void} - */ - handleSectionLinkClick: function(e) { - const addToPlayListObj = this.addToPlayListObj; - - if (e.target.tagName.toLowerCase() === 'a') { - // Only populate new form values if the media player is the same type - // because if it's a different player type (ie. say audio, then the form - // will be reset automatically) - const incomingIsVideo = e.target.dataset['isVideo'] === 'true'; - if (addToPlayListObj.formHasDefinedInputs() && incomingIsVideo === addToPlayListObj.isVideo) { - addToPlayListObj.populateFormValues.apply(this); - } - } - }, - - /** - * Populate all form fields with default values - * @function populateFormValues - * @return {void} - */ - populateFormValues: function() { - const t = this; - let startTime = ''; - let endTime = ''; - let player = t.addToPlayListObj.player; - let formInputs = t.addToPlayListObj.formInputs; - - formInputs.title.value = t.addToPlayListObj.createDefaultPlaylistTitle.apply( - t - ); - formInputs.description.value = ''; - startTime = player.getCurrentTime(); - formInputs.start.value = mejs.Utils.secondsToTimeCode(startTime, true, false, 25, 3); - - // Calculate end value - if ( - $('a.current-stream').length > 0 && - typeof $('a.current-stream')[0].dataset.fragmentend !== 'undefined' - ) { - endTime = parseFloat($('a.current-stream')[0].dataset.fragmentend); - } else { - endTime = player.media.duration; - } - formInputs.end.value = mejs.Utils.secondsToTimeCode(endTime, true, false, 25, 3); - }, - - /** - * Reset all form fields to initial values - * @function resetForm - * @return {void} - */ - resetForm: function() { - const t = this; - let formInputs = t.formInputs; - - for (let prop in formInputs) { - if (formInputs[prop] !== null && prop !== 'playlist') { - formInputs[prop].value = ''; - } - } - t.active = false; - } - } -}); diff --git a/app/assets/javascripts/mejs4_player.js b/app/assets/javascripts/mejs4_player.js index 72d9219c99..fe86487d77 100644 --- a/app/assets/javascripts/mejs4_player.js +++ b/app/assets/javascripts/mejs4_player.js @@ -18,8 +18,6 @@ //= require mediaelement/plugins/markers //= require mediaelement/plugins/quality-avalon //= require mediaelement/plugins/quality-i18n -//= require media_player_wrapper/mejs4_plugin_add_to_playlist -//= require media_player_wrapper/mejs4_plugin_add_marker_to_playlist //= require media_player_wrapper/mejs4_plugin_track_scrubber //= require media_player_wrapper/mejs4_link_back //= require media_player_wrapper/mejs4_plugin_playlist_items diff --git a/app/assets/javascripts/ramp_utils.js b/app/assets/javascripts/ramp_utils.js index 0cbb1ad6ad..ab9cd78e07 100644 --- a/app/assets/javascripts/ramp_utils.js +++ b/app/assets/javascripts/ramp_utils.js @@ -23,20 +23,15 @@ function getTimelineScopes(title) { let scopes = new Array(); let trackCount = 1; - let currentPlayer = document.getElementById('iiif-media-player'); - let duration = currentPlayer.player.duration(); let currentStructureItem = $('li[class="ramp--structured-nav__list-item active"]'); - - let item = currentStructureItem[0].childNodes[1] - let label = item.text; - let times = item.hash.split('#t=').reverse()[0]; - let begin = parseFloat(times.split(',')[0]) || 0; - let end = parseFloat(times.split(',')[1]) || duration; - let streamId = item.pathname.split('/').reverse()[0]; + let activeItem = getActiveItem(); + let streamId = activeItem.streamId; + scopes.push({ - label: label, + label: activeItem.label, tracks: trackCount, - t: `t=${begin},${end}`, + times: activeItem.times, + tag: 'current-track', }); let parent = currentStructureItem.closest('ul').closest('li'); @@ -44,20 +39,45 @@ function getTimelineScopes(title) { let next = parent.closest('ul').closest('li'); let tracks = parent.find('li a'); trackCount = tracks.length; - begin = parseFloat(tracks[0].hash.split('#t=').reverse()[0].split(',')[0]) || 0; - end = parseFloat(tracks[trackCount - 1].hash.split('#t=').reverse()[0].split(',')[1]) || ''; + let begin = parseFloat(tracks[0].hash.split('#t=').reverse()[0].split(',')[0]) || 0; + let end = parseFloat(tracks[trackCount - 1].hash.split('#t=').reverse()[0].split(',')[1]) || ''; streamId = tracks[0].pathname.split('/').reverse()[0]; - label = parent[0].childNodes[0].textContent; + let label = cleanLabel( + parent[0].childNodes[0].textContent, + parent.find('.ramp--structured-nav__section-duration') + ); scopes.push({ label: next.length == 0 ? `${title} - ${label}` : label, tracks: trackCount, - t: `t=${begin},${end}`, + times: { begin, end }, }); parent = next; } return { scopes: scopes.reverse(), streamId }; } +/** + * Clean label text from structured navigation + * @param {String} label full label text of active item + * @param {Object} timestamp HTML span element with duration for section items + * @returns {String} label without index numbers and duration information + */ +function cleanLabel(label, timestamp) { + let labelWoIndex = label.replace(/^[0-9]+./, ''); + if(timestamp?.length > 0) { + let time = timestamp[0].textContent; + return labelWoIndex.replace(time, ''); + } else { + return labelWoIndex.split(' (')[0]; + } +} + +/** + * Parse time in seconds to hh:mm:ss.ms format + * @param {Number} secTime time in seconds + * @param {Boolean} showHrs flag indicating for showing hours + * @returns + */ function createTimestamp(secTime, showHrs) { let hours = Math.floor(secTime / 3600); let minutes = Math.floor((secTime % 3600) / 60); @@ -96,3 +116,135 @@ function updateShareLinks (e) { .attr('placeholder', ltiShareLink); $('#embed-part').val(embedCode); } + +/** Collapse multi item check for creating a playlist item for each structure item of + * the selected scope + */ +function collapseMultiItemCheck () { + $('#multiItemCheck').collapse('show'); + $('#moreDetails').collapse('hide'); +} + +/** Collapse title and description forms */ +function collapseMoreDetails() { + $('#moreDetails').collapse('show'); + $('#multiItemCheck').collapse('hide'); + let currentTrackName = $('#current-track-name').text(); + $('#playlist_item_title').val(currentTrackName); +} + +/** AJAX request for add to playlist for submission for playlist item for + * a selected clip + */ +function addPlaylistItem (playlistId, masterfileId) { + $.ajax({ + url: '/playlists/' + playlistId + '/items', + type: 'POST', + data: { + playlist_item: { + master_file_id: masterfileId, + title: $('#playlist_item_title').val(), + comment: $('#playlist_item_description').val(), + start_time: $('#playlist_item_start').val(), + end_time: $('#playlist_item_end').val() + } + }, + success: function(res) { + handleAddSuccess(res); + }, + error: function(err) { + handleAddError(err) + } + }); +} + +/** AJAX request for add to playlist for submission for playlist items for + * section(s) + */ +function addToPlaylist(playlistId, scope, masterfileId, moId) { + $.ajax({ + url: '/media_objects/' + moId + '/add_to_playlist', + type: 'POST', + data: { + post: { + masterfile_id: masterfileId, + playlist_id: playlistId, + playlistitem_scope: scope + } + }, + success: function(res) { + handleAddSuccess(res); + }, + error: function(err) { + handleAddError(err) + } + }); +} + +/** Show success message for add to playlist */ +function handleAddSuccess(response) { + let alertEl = $('#add_to_playlist_alert'); + + alertEl.removeClass('alert-danger'); + alertEl.addClass('alert-success'); + alertEl.find('#add_to_playlist_result_message').html(response.message); + + alertEl.slideDown(); + $('#add_to_playlist_form_group').slideUp(); + resetAddToPlaylistForm(); +} + +/** Show error message for add to playlist */ +function handleAddError(error) { + let alertEl = $('#add_to_playlist_alert'); + let message = error.statusText || 'There was an error adding to playlist'; + + if (error.responseJSON && error.responseJSON.message) { + message = error.responseJSON.message.join('
'); + } + + alertEl.removeClass('alert-success'); + alertEl.addClass('alert-danger add_to_playlist_alert_error'); + alertEl.find('#add_to_playlist_result_message').html('ERROR: ' + message); + + alertEl.slideDown(); + $('#add_to_playlist_form_group').slideUp(); + resetAddToPlaylistForm(); +} + +/** Reset add to playlist form */ +function resetAddToPlaylistForm() { + $('#playlist_item_start')[0].value = ''; + $('#playlist_item_end')[0].value = ''; + $('#playlist_item_description').value = ''; + $('#playlist_item_title').value = ''; + $('input[name="post[playlistitem_scope]"]').prop('checked', false); + $('#playlistitem_scope_structure').prop('checked', false); + $('#moreDetails').collapse('hide'); + $('#multiItemCheck').collapse('hide'); +} + +/** Reset add to playlist panel when alert is closed */ +function closeAlert() { + $('#add_to_playlist_alert').slideUp(); + $('#add_to_playlist_form_group').slideDown(); +} + +/** Get the current active structure item from DOM */ +function getActiveItem() { + let currentPlayer = document.getElementById('iiif-media-player'); + let duration = currentPlayer.player.duration(); + let currentStructureItem = $('li[class="ramp--structured-nav__list-item active"]'); + if(currentStructureItem.find('a').length > 0) { + let item = currentStructureItem.find('a')[0]; + let label = cleanLabel(item.text, + currentStructureItem.find('.ramp--structured-nav__section-duration')); + let timeHash = item.hash.split('#t=').reverse()[0]; + let times = { + begin: parseFloat(timeHash.split(',')[0]) || 0, + end: parseFloat(timeHash.split(',')[1]) || duration + } + let streamId = item.pathname.split('/').reverse()[0]; + return { label, times, streamId }; + } +} diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 5f17e0faf2..2abcb58e3f 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -486,9 +486,6 @@ a[data-trigger='submit'] { margin-bottom: 0; } -#share-list .tab-content { - margin-bottom: 20px; -} .ready-to-play { .structure.current-stream:before { diff --git a/app/assets/stylesheets/avalon/_buttons.scss b/app/assets/stylesheets/avalon/_buttons.scss index 8b5f023328..d7c2926752 100644 --- a/app/assets/stylesheets/avalon/_buttons.scss +++ b/app/assets/stylesheets/avalon/_buttons.scss @@ -28,7 +28,6 @@ button.close { #share-button { text-align: right; - margin-top: 10px; margin-bottom: 0; a:link, @@ -38,3 +37,8 @@ button.close { text-decoration: none; } } + +.add-to-playlist-form-buttons { + display: flex; + justify-content: flex-end; +} diff --git a/app/assets/stylesheets/mejs4/mejs4_plugin_add_to_playlist.scss b/app/assets/stylesheets/mejs4/mejs4_plugin_add_to_playlist.scss deleted file mode 100644 index e6e4ef297b..0000000000 --- a/app/assets/stylesheets/mejs4/mejs4_plugin_add_to_playlist.scss +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2011-2023, The Trustees of Indiana University and Northwestern - * University. Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * --- END LICENSE_HEADER BLOCK --- -*/ - -/** - * Stylesheet for Avalon custom MediaElement 4 plugin: "Add To Playlist". - */ - -$avalon-primary-color: #84a791; - -// Control button on MEJS player -.mejs-add-to-playlist-button > button, -.mejs__add-to-playlist-button > button { - background-position: -50px -20px; - width: 25px; -} - -.add_playlist_item_playlists_no_playlists_message { - a { - color: $avalon-primary-color !important; - } -} - -.playlist-visibility-form-group { - > label:first-of-type { - display: block; - } -} diff --git a/app/assets/stylesheets/mejs4_player.scss b/app/assets/stylesheets/mejs4_player.scss index 8553854e9e..dab50ad3ec 100644 --- a/app/assets/stylesheets/mejs4_player.scss +++ b/app/assets/stylesheets/mejs4_player.scss @@ -18,8 +18,6 @@ *= require mediaelement/mediaelementplayer.scss *= require mediaelement/plugins/quality.css *= require mejs4/mediaelement-common-styles.scss - *= require mejs4/mejs4_plugin_add_to_playlist.scss - *= require mejs4/mejs4_plugin_add_marker_to_playlist.scss *= require mejs4/mejs4_plugin_create_thumbnail.scss *= require mejs4/mejs4_plugin_track_scrubber.scss *= require mejs4/mejs4_link_back.scss diff --git a/app/controllers/media_objects_controller.rb b/app/controllers/media_objects_controller.rb index 78e3d39b20..4ad00220e0 100644 --- a/app/controllers/media_objects_controller.rb +++ b/app/controllers/media_objects_controller.rb @@ -24,8 +24,8 @@ class MediaObjectsController < ApplicationController include SecurityHelper before_action :authenticate_user!, except: [:show, :set_session_quality, :show_stream_details, :manifest] - before_action :load_resource, except: [:create, :destroy, :update_status, :set_session_quality, :tree, :deliver_content, :confirm_remove, :show_stream_details, :add_to_playlist_form, :add_to_playlist, :intercom_collections, :manifest, :move_preview, :edit, :update, :json_update] - load_and_authorize_resource except: [:create, :destroy, :update_status, :set_session_quality, :tree, :deliver_content, :confirm_remove, :show_stream_details, :add_to_playlist_form, :add_to_playlist, :intercom_collections, :manifest, :move_preview, :show_progress] + before_action :load_resource, except: [:create, :destroy, :update_status, :set_session_quality, :tree, :deliver_content, :confirm_remove, :show_stream_details, :add_to_playlist, :intercom_collections, :manifest, :move_preview, :edit, :update, :json_update] + load_and_authorize_resource except: [:create, :destroy, :update_status, :set_session_quality, :tree, :deliver_content, :confirm_remove, :show_stream_details, :add_to_playlist, :intercom_collections, :manifest, :move_preview, :show_progress] authorize_resource only: [:create] before_action :inject_workflow_steps, only: [:edit, :update], unless: proc { request.format.json? } @@ -107,17 +107,6 @@ def new redirect_to edit_media_object_path(@media_object) end - # POST /media_objects/avalon:1/add_to_playlist_form - def add_to_playlist_form - @media_object = SpeedyAF::Proxy::MediaObject.find(params[:id]) - authorize! :read, @media_object - respond_to do |format| - format.html do - render partial: 'add_to_playlist_form', locals: { scope: params[:scope], masterfile_id: params[:masterfile_id] } - end - end - end - # POST /media_objects/avalon:1/add_to_playlist def add_to_playlist @media_object = SpeedyAF::Proxy::MediaObject.find(params[:id]) diff --git a/app/javascript/components/Ramp.jsx b/app/javascript/components/Ramp.jsx index da7f15b7b0..32e7df27c1 100644 --- a/app/javascript/components/Ramp.jsx +++ b/app/javascript/components/Ramp.jsx @@ -63,13 +63,54 @@ const Ramp = ({
{
}
-
- { timeline.canCreate &&
} - { playlist.canCreate &&
} - { share.canShare &&
} - { admin_links.canUpdate &&
} - { thumbnail.canCreate &&
} -
+
+ + { timeline.canCreate &&
} + { playlist.canCreate && + + } + { share.canShare && + + } + + + { admin_links.canUpdate &&
} + { thumbnail.canCreate &&
} + +
+ + +
+
+
+ + +
+
+
+ +
{
}
diff --git a/app/javascript/components/Ramp.scss b/app/javascript/components/Ramp.scss index c61d1406e6..3257f2f034 100644 --- a/app/javascript/components/Ramp.scss +++ b/app/javascript/components/Ramp.scss @@ -44,44 +44,25 @@ } .ramp--rails-content { + margin: 14px 0; display: flex; + justify-content: space-between; - #administrative_options { - text-align: right !important; - } - - .btn-sm { - padding: 0 0; - } - - .share-tabs { - flex-grow: 2; - } - - #share-list { - margin-left: -9.25rem; - } - - .svg-add-to-playlist { - width: 35px; - margin: 0; - } - - .bgColor { - fill: white; + #shareBtn{ + i { + margin-right: 0.2rem; + color: #2a5459; + } } - #add-to-playlist-btn:hover { - .bgColor { - fill: #f2f2f2; - } + .ramp-button-group-1 { + display: flex; + justify-content: flex-start; } - .foregroundColor { - fill: none; - stroke: #2a5459; - stroke-width: 2; - stroke-miterlimit: 10; + .ramp-button-group-2 { + display: flex; + justify-content: flex-end; } } @@ -211,3 +192,14 @@ height: fit-content !important; } } + +#add_to_playlist_alert { + a { + font-weight: bold; + text-decoration: underline; + } + + p { + margin: 0; + } +} diff --git a/app/javascript/components/collections/Collection.scss b/app/javascript/components/collections/Collection.scss index 3cf123e7a6..030a599bd1 100644 --- a/app/javascript/components/collections/Collection.scss +++ b/app/javascript/components/collections/Collection.scss @@ -184,17 +184,14 @@ margin-top: 7px; } -.mb-3 { - margin-bottom: 8px; - margin-top: 8px; -} - .mr-2 { margin-right: 2px; } .facet-badges { margin-left: 16px; + margin-bottom: 8px; + margin-top: 8px; } @media (max-width: 24em) { diff --git a/app/views/media_objects/_add_to_playlist.html.erb b/app/views/media_objects/_add_to_playlist.html.erb new file mode 100644 index 0000000000..a342b913a4 --- /dev/null +++ b/app/views/media_objects/_add_to_playlist.html.erb @@ -0,0 +1,258 @@ +<%# +Copyright 2011-2023, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> + + +<% @add_playlist_item_playlists = Playlist.where(user: current_user).sort_by(&:title) %> + +
+
+ +

+
+
+ <% unless @add_playlist_item_playlists.empty? %> +
+
+
+

Add to Playlist

+
+
+
+ <%= collection_select(:post, :playlist_id, @add_playlist_item_playlists, :id, :title, {}, {class: "form-control form-model", style: 'width:100%;'}) %> +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+ <% end %> + <% if @add_playlist_item_playlists.empty? %> + You have no playlists, <%= link_to('create a playlist.', new_playlist_path) %> + <% end %> +
+
+ + + + + +<% content_for :page_scripts do %> + +<% end %> diff --git a/app/views/media_objects/_add_to_playlist_form.html.erb b/app/views/media_objects/_add_to_playlist_form.html.erb deleted file mode 100644 index 57e50129a0..0000000000 --- a/app/views/media_objects/_add_to_playlist_form.html.erb +++ /dev/null @@ -1,67 +0,0 @@ -<%# -Copyright 2011-2023, The Trustees of Indiana University and Northwestern - University. Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. ---- END LICENSE_HEADER BLOCK --- -%> - - - - diff --git a/app/views/media_objects/_administrative_links.html.erb b/app/views/media_objects/_administrative_links.html.erb index 9efdd07f99..5d2db4bef7 100644 --- a/app/views/media_objects/_administrative_links.html.erb +++ b/app/views/media_objects/_administrative_links.html.erb @@ -14,7 +14,7 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% if can? :update, @media_object %> -
+
<%= link_to 'Edit', edit_media_object_path(@media_object), class: 'btn btn-primary' %> <% if @media_object.published? %> diff --git a/app/views/media_objects/_item_view.html.erb b/app/views/media_objects/_item_view.html.erb index 025b725895..f0e5ae7d99 100644 --- a/app/views/media_objects/_item_view.html.erb +++ b/app/views/media_objects/_item_view.html.erb @@ -40,7 +40,7 @@ Unless required by applicable law or agreed to in writing, software distributed share: { canShare: (will_partial_list_render? :share), content: lending_enabled?(@media_object) ? (render('share') if can_stream) : render('share') }, timeline: { canCreate: (current_ability.can? :create, Timeline), content: lending_enabled?(@media_object) ? (render('timeline') if can_stream) : render('timeline') }, thumbnail: { canCreate: (current_ability.can? :edit, @media_object), content: lending_enabled?(@media_object) ? (render('thumbnail') if can_stream) : render('thumbnail') }, - playlist: { canCreate: (current_ability.can? :create, Playlist), content: lending_enabled?(@media_object) ? (render('playlist') if can_stream) : render('playlist') }, + playlist: { canCreate: (current_ability.can? :create, Playlist), tab: render('add_to_playlist') }, in_progress: in_progress, cdl: { enabled: lending_enabled?(@media_object), can_stream: can_stream, embed: render('embed_checkout'), destroy: render('destroy_checkout') } } diff --git a/app/views/media_objects/_mejs4_add_to_playlist.html.erb b/app/views/media_objects/_mejs4_add_to_playlist.html.erb deleted file mode 100644 index b54f983753..0000000000 --- a/app/views/media_objects/_mejs4_add_to_playlist.html.erb +++ /dev/null @@ -1,115 +0,0 @@ -<%# -Copyright 2011-2023, The Trustees of Indiana University and Northwestern - University. Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. ---- END LICENSE_HEADER BLOCK --- -%> - -<%# -This view file goes with the custom 'Add To Playlist' MediaElement 4 plugin. -%> -<% content_for :page_scripts do %> - <%= javascript_include_tag 'select2.min' %> - <%= stylesheet_link_tag 'select2.min' %> -<% end %> - -<% # Get users playlists %> -<% @add_playlist_item_playlists = Playlist.where(user: current_user).sort_by(&:title) %> - - - - - - diff --git a/app/views/media_objects/_playlist.html.erb b/app/views/media_objects/_playlist.html.erb deleted file mode 100644 index ac4e067221..0000000000 --- a/app/views/media_objects/_playlist.html.erb +++ /dev/null @@ -1,37 +0,0 @@ -<%# -Copyright 2011-2023, The Trustees of Indiana University and Northwestern - University. Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. ---- END LICENSE_HEADER BLOCK --- -%> - -
- -
- -<% # TODO: Build add to playlist modal %> diff --git a/app/views/media_objects/_share.html.erb b/app/views/media_objects/_share.html.erb index efbacda6c8..92a1fb843f 100644 --- a/app/views/media_objects/_share.html.erb +++ b/app/views/media_objects/_share.html.erb @@ -14,13 +14,7 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> - - -
+
@@ -38,7 +32,7 @@ Unless required by applicable law or agreed to in writing, software distributed const event = new CustomEvent('canvasswitch', { detail: { lti_share_link: '', link_back_url: '', embed_code: '' } }) function canvasIndexListener() { let player = document.getElementById('iiif-media-player'); - if (player && player != undefined) { + if (player && player.player != undefined) { player.player.on("loadedmetadata", () => { if (player.dataset.canvasindex != canvasIndex) { canvasIndex = parseInt(player.dataset.canvasindex); @@ -62,6 +56,11 @@ Unless required by applicable law or agreed to in writing, software distributed player.addEventListener("canvasswitch", (e) => { updateShareLinks(e); }); + + // Hide add to playlist panel when share resource panel is collapsed + $('#shareResourcePanel').on('show.bs.collapse', function (e) { + $('#addToPlaylistPanel').collapse('hide'); + }); } if(!$('.share-tabs li').first().hasClass('active')) { $('.share-tabs li').first().toggleClass('active'); diff --git a/app/views/media_objects/_thumbnail.html.erb b/app/views/media_objects/_thumbnail.html.erb index d11b5a4174..74c66d02a6 100644 --- a/app/views/media_objects/_thumbnail.html.erb +++ b/app/views/media_objects/_thumbnail.html.erb @@ -15,7 +15,7 @@ Unless required by applicable law or agreed to in writing, software distributed %>
-
@@ -54,7 +54,9 @@ Unless required by applicable law or agreed to in writing, software distributed if(player) { player.player.on('loadedmetadata', () => { let thumbnailBtn = document.getElementById('create-thumbnail-btn'); - thumbnailBtn.disabled = false; + if(thumbnailBtn) { + thumbnailBtn.disabled = false; + } clearInterval(timeCheck); }); } diff --git a/app/views/media_objects/_timeline.html.erb b/app/views/media_objects/_timeline.html.erb index d7801b3c25..c40fc2f603 100644 --- a/app/views/media_objects/_timeline.html.erb +++ b/app/views/media_objects/_timeline.html.erb @@ -14,8 +14,8 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> -
-
@@ -58,7 +58,9 @@ $(document).ready(function() { if(player) { player.player.on('loadedmetadata', () => { let timelineBtn = document.getElementById('timeline-btn'); - timelineBtn.disabled = false; + if(timelineBtn) { + timelineBtn.disabled = false; + } clearInterval(timeCheck); }); } @@ -120,7 +122,8 @@ $(document).ready(function() { if (selectedIndex === -1) return; scope = scopes[selectedIndex]; label = scope.label; - t = scope.t + let { begin, end } = scope.times; + t = `t=${begin},${end}`; id = streamId; } $('#new-timeline-title')[0].value = label; diff --git a/config/routes.rb b/config/routes.rb index d2e8bb406d..d42b05e898 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -123,7 +123,6 @@ get 'section/:content/embed', :to => redirect('/master_files/%{content}/embed') get 'tree', :action => :tree, :as => :tree get :confirm_remove - get :add_to_playlist_form post :add_to_playlist patch :intercom_push get :manifest diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index aaab3b9177..9a3752449c 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -75,7 +75,6 @@ expect(get :tree, params: { id: media_object.id }).to render_template('errors/restricted_pid') expect(get :deliver_content, params: { id: media_object.id, file: 'descMetadata' }).to render_template('errors/restricted_pid') expect(delete :destroy, params: { id: media_object.id }).to render_template('errors/restricted_pid') - expect(get :add_to_playlist_form, params: { id: media_object.id }).to render_template('errors/restricted_pid') expect(post :add_to_playlist, params: { id: media_object.id }).to render_template('errors/restricted_pid') end it "json routes should return 401" do @@ -103,7 +102,6 @@ expect(put :update, params: { id: media_object.id }).to render_template('errors/restricted_pid') expect(get :tree, params: { id: media_object.id }).to render_template('errors/restricted_pid') expect(get :deliver_content, params: { id: media_object.id, file: 'descMetadata' }).to render_template('errors/restricted_pid') - expect(get :add_to_playlist_form, params: { id: media_object.id }).to render_template('errors/restricted_pid') expect(post :add_to_playlist, params: { id: media_object.id }).to render_template('errors/restricted_pid') end it "json routes should return 401" do @@ -1721,37 +1719,6 @@ end end - describe "#add_to_playlist_form" do - let(:media_object) { FactoryBot.create(:fully_searchable_media_object, :with_master_file) } - - before do - login_as :user - end - it "should render add_to_playlist_form with correct masterfile_id" do - get :add_to_playlist_form, params: { id: media_object.id, scope: 'master_file', masterfile_id: media_object.ordered_master_file_ids[0] } - expect(response).to render_template(:_add_to_playlist_form) - expect(response.body).to include(media_object.ordered_master_file_ids[0]) - end - it "should render the correct label for scope=master_file" do - get :add_to_playlist_form, params: { id: media_object.id, scope: 'master_file', masterfile_id: media_object.ordered_master_file_ids[0] } - expect(response.body).to include('Add Section to Playlist') - end - it "should render the correct label for scope=media_object" do - get :add_to_playlist_form, params: { id: media_object.id, scope: 'media_object', masterfile_id: media_object.ordered_master_file_ids[0] } - expect(response.body).to include('Add Item to Playlist') - end - - context 'read from solr' do - it 'should not read from fedora' do - media_object - perform_enqueued_jobs(only: MediaObjectIndexingJob) - WebMock.reset_executed_requests! - get :add_to_playlist_form, params: { id: media_object.id, scope: 'media_object', masterfile_id: media_object.ordered_master_file_ids[0] } - expect(a_request(:any, /#{ActiveFedora.fedora.base_uri}/)).not_to have_been_made - end - end - end - describe "#add_to_playlist" do let(:media_object) { FactoryBot.create(:fully_searchable_media_object, title: 'Test Item') } let(:master_file) { FactoryBot.create(:master_file, media_object: media_object, title: 'Test Section') }