Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Duplicate" feature to mediamanager #1083

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
8 changes: 8 additions & 0 deletions modules/backend/lang/en/lang.php
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@
'menu_label' => 'Media',
'upload' => 'Upload',
'move' => 'Move',
'duplicate' => 'Duplicate',
'delete' => 'Delete',
'add_folder' => 'Add folder',
'search' => 'Search',
Expand Down Expand Up @@ -631,10 +632,17 @@
'direction' => 'Direction',
'direction_asc' => 'Ascending',
'direction_desc' => 'Descending',
'file_exists_autorename' => 'The file already exists. Renamed to: :name',
'folder' => 'Folder',
'folder_exists_autorename' => 'The folder already exists. Renamed to: :name',
'no_files_found' => 'No files found by your request.',
'delete_empty' => 'Please select items to delete.',
'delete_confirm' => 'Delete the selected item(s)?',
'duplicate_empty' => 'Please select items to duplicate.',
'duplicate_multiple_confirm' => 'Multiple items selected. They will be duplicated with generated names. Are you sure?',
'duplicate_popup_title' => 'Duplicate file or folder',
'duplicate_new_name' => 'New name',
'duplicate_button' => 'Duplicate',
'error_renaming_file' => 'Error renaming the item.',
'new_folder_title' => 'New folder',
'folder_name' => 'Folder name',
Expand Down
8 changes: 8 additions & 0 deletions modules/backend/lang/fr/lang.php
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@
'menu_label' => 'Média',
'upload' => 'Déposer un fichier',
'move' => 'Déplacer',
'duplicate' => 'Dupliquer',
'delete' => 'Supprimer',
'add_folder' => 'Ajouter un répertoire',
'search' => 'Rechercher',
Expand Down Expand Up @@ -624,10 +625,17 @@
'direction' => 'Direction',
'direction_asc' => 'Ascendant',
'direction_desc' => 'Descendant',
'file_exists_autorename' => 'Le fichier existe déjà. Renommé en : :name',
'folder' => 'Répertoire',
'folder_exists_autorename' => 'Le dossier existe déjà. Renommé en : :name',
'no_files_found' => 'Aucun fichier trouvé.',
'delete_empty' => 'Veuillez sélectionner les éléments à supprimer.',
'delete_confirm' => 'Confirmer la suppression de ces éléments ?',
'duplicate_empty' => 'Veuillez sélectionner les éléments à dupliquer.',
'duplicate_multiple_confirm' => 'Plusieurs éléments sélectionnés. Ils seront clonés avec des noms générés. Êtes-vous sûr ?',
'duplicate_popup_title' => 'Dupliquer un fichier ou dossier',
'duplicate_new_name' => 'Nouveau nom',
'duplicate_button' => 'Dupliquer',
'error_renaming_file' => 'Erreur lors du renommage de l\'élément.',
'new_folder_title' => 'Nouveau répertoire',
'folder_name' => 'Nom du répertoire',
Expand Down
246 changes: 246 additions & 0 deletions modules/backend/widgets/MediaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use System\Classes\ImageResizer;
use System\Classes\MediaLibrary;
use System\Classes\MediaLibraryItem;
use Winter\Storm\Support\Facades\Flash;

/**
* Media Manager widget.
Expand Down Expand Up @@ -520,6 +521,224 @@ public function onCreateFolder(): array
];
}

/**
* Render the duplicate popup for the provided "path" from the request
*/
public function onLoadDuplicatePopup(): string
{
$this->abortIfReadOnly();

$path = Input::get('path');
$type = Input::get('type');
$path = MediaLibrary::validatePath($path);
$suggestedName = '';

$library = MediaLibrary::instance();

if ($type == MediaLibraryItem::TYPE_FILE) {
$suggestedName = $library->generateIncrementedFileName($path);
} else {
$suggestedName = $library->generateIncrementedFolderName($path);
}

$this->vars['originalPath'] = $path;
$this->vars['newName'] = $suggestedName;
$this->vars['type'] = $type;

return $this->makePartial('duplicate-form');
}

/**
* Duplicate the provided path from the request ("originalPath") to the new name ("name")
*
* @throws ApplicationException if the new name is invalid
*/
public function onDuplicateItem(): array
{
$this->abortIfReadOnly();

$newName = Input::get('newName');
if (!strlen($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty'));
}

if (!$this->validateFileName($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name'));
}


$originalPath = Input::get('originalPath');
$originalPath = MediaLibrary::validatePath($originalPath);
$newPath = dirname($originalPath) . '/' . $newName;
$type = Input::get('type');

$newPath = $this->preventPathOverwrite($originalPath, $newPath, $type);

$library = MediaLibrary::instance();

if ($type == MediaLibraryItem::TYPE_FILE) {
/*
* Validate extension
*/
if (!$this->validateFileType($newName)) {
throw new ApplicationException(Lang::get('backend::lang.media.type_blocked'));
}

/*
* Duplicate single file
*/
$library->copyFile($originalPath, $newPath);

/**
* @event media.file.duplicate
* Called after a file is duplicated
*
* Example usage:
*
* Event::listen('media.file.duplicate', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $originalPath, (string) $newPath) {
* \Log::info($originalPath . " was duplicated to " . $path);
* });
*
* Or
*
* $mediaWidget->bindEvent('file.duplicate', function ((string) $originalPath, (string) $newPath) {
* \Log::info($originalPath . " was duplicated to " . $path);
* });
*
*/
$this->fireSystemEvent('media.file.duplicate', [$originalPath, $newPath]);
} else {
/*
* Duplicate single folder
*/
$library->copyFolder($originalPath, $newPath);

/**
* @event media.folder.duplicate
* Called after a folder is duplicated
*
* Example usage:
*
* Event::listen('media.folder.duplicate', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $originalPath, (string) $newPath) {
* \Log::info($originalPath . " was duplicated to " . $path);
* });
*
* Or
*
* $mediaWidget->bindEvent('folder.duplicate', function ((string) $originalPath, (string) $newPath) {
* \Log::info($originalPath . " was duplicated to " . $path);
* });
*
*/
$this->fireSystemEvent('media.folder.duplicate', [$originalPath, $newPath]);
}

$library->resetCache();
$this->prepareVars();

return [
'#' . $this->getId('item-list') => $this->makePartial('item-list')
];
}

/**
* Duplicate the selected files or folders without prompting the user
* The new name will be generated in an incremented sequence
*
* @throws ApplicationException if the input data is invalid
*/
public function onDuplicateItems(): array
{
$this->abortIfReadOnly();

$paths = Input::get('paths');

if (!is_array($paths)) {
throw new ApplicationException('Invalid input data');
}

$library = MediaLibrary::instance();

$filesToDuplicate = [];
foreach ($paths as $pathInfo) {
$path = array_get($pathInfo, 'path');
$type = array_get($pathInfo, 'type');

if (!$path || !$type) {
throw new ApplicationException('Invalid input data');
}

if ($type === MediaLibraryItem::TYPE_FILE) {
/*
* Add to bulk collection
*/
$filesToDuplicate[] = $path;
} elseif ($type === MediaLibraryItem::TYPE_FOLDER) {
/*
* Duplicate single folder
*/
$library->duplicateFolder($path);

/**
* @event media.folder.duplicate
* Called after a folder is duplicated
*
* Example usage:
*
* Event::listen('media.folder.duplicate', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $path) {
* \Log::info($path . " was duplicated");
* });
*
* Or
*
* $mediaWidget->bindEvent('folder.duplicate', function ((string) $path) {
* \Log::info($path . " was duplicated");
* });
*
*/
$this->fireSystemEvent('media.folder.duplicate', [$path]);
}
}

if (count($filesToDuplicate) > 0) {
/*
* Duplicate collection of files
*/
$library->duplicateFiles($filesToDuplicate);

/*
* Extensibility
*/
foreach ($filesToDuplicate as $path) {
/**
* @event media.file.duplicate
* Called after a file is duplicated
*
* Example usage:
*
* Event::listen('media.file.duplicate', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $path) {
* \Log::info($path . " was duplicated");
* });
*
* Or
*
* $mediaWidget->bindEvent('file.duplicate', function ((string) $path) {
* \Log::info($path . " was duplicated");
* });
*
*/
$this->fireSystemEvent('media.file.duplicate', [$path]);
}
}

$library->resetCache();
$this->prepareVars();

return [
'#' . $this->getId('item-list') => $this->makePartial('item-list')
];
}

/**
* Render the move popup with a list of folders to move the selected items to excluding the provided paths in the request ("exclude")
*
Expand Down Expand Up @@ -1376,4 +1595,31 @@ protected function getPreferenceKey()
// User preferences should persist across controller usages for the MediaManager
return "backend::widgets.media_manager." . strtolower($this->getId());
}

/**
* Check if file or folder already exists, then return an incremented name to prevent overwriting
*
* @param string $originalPath
* @param string $newPath
* @param string $type
*
* @todo Maybe the overwriting behavior can be config based
*/
protected function preventPathOverwrite($originalPath, $newPath, $type): string
{
$library = MediaLibrary::instance();

if ($library->exists($newPath)) {
if ($type == MediaLibraryItem::TYPE_FILE) {
$newName = $library->generateIncrementedFileName($originalPath);
} else {
$newName = $library->generateIncrementedFolderName($originalPath);
}
$newPath = dirname($originalPath) . '/' . $newName;

Flash::info(Lang::get('backend::lang.media.'. $type .'_exists_autorename', ['name' => $newName]));
}

return $newPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ this.$el.on('input','[data-control="search"]',this.proxy(this.onSearchChanged))
this.$el.on('mediarefresh',this.proxy(this.refresh))
this.$el.on('shown.oc.popup','[data-command="create-folder"]',this.proxy(this.onFolderPopupShown))
this.$el.on('hidden.oc.popup','[data-command="create-folder"]',this.proxy(this.onFolderPopupHidden))
this.$el.on('shown.oc.popup','[data-command="duplicate"]',this.proxy(this.onDuplicatePopupShown))
this.$el.on('hidden.oc.popup','[data-command="duplicate"]',this.proxy(this.onDuplicatePopupHidden))
this.$el.on('shown.oc.popup','[data-command="move"]',this.proxy(this.onMovePopupShown))
this.$el.on('hidden.oc.popup','[data-command="move"]',this.proxy(this.onMovePopupHidden))
this.$el.on('keydown',this.proxy(this.onKeyDown))
Expand All @@ -77,6 +79,8 @@ this.$el.off('change','[data-control="sorting"]',this.proxy(this.onSortingChange
this.$el.off('keyup','[data-control="search"]',this.proxy(this.onSearchChanged))
this.$el.off('shown.oc.popup','[data-command="create-folder"]',this.proxy(this.onFolderPopupShown))
this.$el.off('hidden.oc.popup','[data-command="create-folder"]',this.proxy(this.onFolderPopupHidden))
this.$el.off('shown.oc.popup','[data-command="duplicate"]',this.proxy(this.onDuplicatePopupShown))
this.$el.off('hidden.oc.popup','[data-command="duplicate"]',this.proxy(this.onDuplicatePopupHidden))
this.$el.off('shown.oc.popup','[data-command="move"]',this.proxy(this.onMovePopupShown))
this.$el.off('hidden.oc.popup','[data-command="move"]',this.proxy(this.onMovePopupHidden))
this.$el.off('keydown',this.proxy(this.onKeyDown))
Expand All @@ -100,8 +104,9 @@ MediaManager.prototype.selectItem=function(node,expandSelection){if(!expandSelec
for(var i=0,len=items.length;i<len;i++){items[i].setAttribute('class','')}node.setAttribute('class','selected')}else{if(node.getAttribute('class')=='selected')node.setAttribute('class','')
else node.setAttribute('class','selected')}node.focus()
this.clearSelectTimer()
if(this.isPreviewSidebarVisible()){this.selectTimer=setTimeout(this.proxy(this.updateSidebarPreview),100)}if(node.hasAttribute('data-root')&&!expandSelection){this.toggleMoveAndDelete(true)}else{this.toggleMoveAndDelete(false)}if(expandSelection){this.unselectRoot()}}
MediaManager.prototype.toggleMoveAndDelete=function(value){$('[data-command=delete]',this.$el).prop('disabled',value)
if(this.isPreviewSidebarVisible()){this.selectTimer=setTimeout(this.proxy(this.updateSidebarPreview),100)}if(node.hasAttribute('data-root')&&!expandSelection){this.toggleMoveDuplicateDelete(true)}else{this.toggleMoveDuplicateDelete(false)}if(expandSelection){this.unselectRoot()}}
MediaManager.prototype.toggleMoveDuplicateDelete=function(value){$('[data-command=delete]',this.$el).prop('disabled',value)
$('[data-command=duplicate]',this.$el).prop('disabled',value)
$('[data-command=move]',this.$el).prop('disabled',value)}
MediaManager.prototype.unselectRoot=function(){var rootItem=this.$el.get(0).querySelector('[data-type="media-item"][data-root].selected')
if(rootItem)rootItem.setAttribute('class','')}
Expand Down Expand Up @@ -283,6 +288,24 @@ ev.preventDefault()
return false}
MediaManager.prototype.folderCreated=function(){this.$el.find('button[data-command="create-folder"]').popup('hide')
this.afterNavigate()}
MediaManager.prototype.duplicateItems=function(ev){var items=this.$el.get(0).querySelectorAll('[data-type="media-item"].selected')
if(!items.length){$.wn.alert(this.options.duplicateEmpty)
return}if(items.length>1){$.wn.confirm(this.options.duplicateMultipleConfirm,this.proxy(this.duplicateMultipleConfirmation))}else{var data={path:items[0].getAttribute('data-path'),type:items[0].getAttribute('data-item-type')}
$(ev.target).popup({handler:this.options.alias+'::onLoadDuplicatePopup',extraData:data,zIndex:1200})}}
MediaManager.prototype.duplicateMultipleConfirmation=function(confirmed){if(!confirmed)return
var items=this.$el.get(0).querySelectorAll('[data-type="media-item"].selected'),paths=[]
for(var i=0,len=items.length;i<len;i++){if(items[i].hasAttribute('data-root')){continue;}paths.push({'path':items[i].getAttribute('data-path'),'type':items[i].getAttribute('data-item-type')})}var data={paths:paths}
$.wn.stripeLoadIndicator.show()
this.$form.request(this.options.alias+'::onDuplicateItems',{data:data}).always(function(){$.wn.stripeLoadIndicator.hide()}).done(this.proxy(this.afterNavigate))}
MediaManager.prototype.onDuplicatePopupShown=function(ev,button,popup){$(popup).on('submit.media','form',this.proxy(this.onDuplicateItemSubmit))}
MediaManager.prototype.onDuplicateItemSubmit=function(ev){var item=this.$el.get(0).querySelector('[data-type="media-item"].selected'),data={newName:$(ev.target).find('input[name=newName]').val(),originalPath:$(ev.target).find('input[name=originalPath]').val(),type:$(ev.target).find('input[name=type]').val()}
$.wn.stripeLoadIndicator.show()
this.$form.request(this.options.alias+'::onDuplicateItem',{data:data}).always(function(){$.wn.stripeLoadIndicator.hide()}).done(this.proxy(this.itemDuplicated))
ev.preventDefault()
return false}
MediaManager.prototype.onDuplicatePopupHidden=function(ev,button,popup){$(popup).off('.media','form')}
MediaManager.prototype.itemDuplicated=function(){this.$el.find('button[data-command="duplicate"]').popup('hide')
this.afterNavigate()}
MediaManager.prototype.moveItems=function(ev){var items=this.$el.get(0).querySelectorAll('[data-type="media-item"].selected')
if(!items.length){$.wn.alert(this.options.moveEmpty)
return}var data={exclude:[],path:this.$el.find('[data-type="current-folder"]').val()}
Expand Down Expand Up @@ -310,6 +333,7 @@ break;case'close-uploader':this.hideUploadUi()
break;case'set-filter':this.setFilter($(ev.currentTarget).data('filter'))
break;case'delete':this.deleteItems()
break;case'create-folder':this.createFolder(ev)
break;case'duplicate':this.duplicateItems(ev)
break;case'move':this.moveItems(ev)
break;case'toggle-sidebar':this.toggleSidebar(ev)
break;case'popup-command':var popupCommand=$(ev.currentTarget).data('popup-command')
Expand Down Expand Up @@ -362,7 +386,7 @@ break;case'ArrowLeft':case'ArrowUp':this.selectRelative(false,ev.shiftKey)
eventHandled=true
break;}if(eventHandled){ev.preventDefault()
ev.stopPropagation()}}
MediaManager.DEFAULTS={url:window.location,uploadHandler:null,alias:'',deleteEmpty:'Please select files to delete.',deleteConfirm:'Delete the selected file(s)?',moveEmpty:'Please select files to move.',selectSingleImage:'Please select a single image.',selectionNotImage:'The selected item is not an image.',bottomToolbar:false,cropAndInsertButton:false}
MediaManager.DEFAULTS={url:window.location,uploadHandler:null,alias:'',duplicateEmpty:'Please select an item to duplicate.',duplicateMultipleConfirm:'Multiple items selected, they will be duplicated with generated names. Are you sure?',deleteEmpty:'Please select files to delete.',deleteConfirm:'Delete the selected file(s)?',moveEmpty:'Please select files to move.',selectSingleImage:'Please select a single image.',selectionNotImage:'The selected item is not an image.',bottomToolbar:false,cropAndInsertButton:false}
var old=$.fn.mediaManager
$.fn.mediaManager=function(option){var args=Array.prototype.slice.call(arguments,1),result=undefined
this.each(function(){var $this=$(this)
Expand Down Expand Up @@ -540,4 +564,4 @@ case'undo-resizing':this.undoResizing()
break}}
MediaManagerImageCropPopup.prototype.onSelectionChanged=function(c){this.updateSelectionSizeLabel(c.w,c.h)}
MediaManagerImageCropPopup.DEFAULTS={alias:undefined,onDone:undefined}
$.wn.mediaManager.imageCropPopup=MediaManagerImageCropPopup}(window.jQuery);
$.wn.mediaManager.imageCropPopup=MediaManagerImageCropPopup}(window.jQuery);
Loading
Loading