Add support for media saving messages in dropzone (#11304)

* WIP

Waiting for a response on the issue about how to proceed

* Support messages in dropzone

Update dropzone to allow the showing of messages that may be added in a media saving notification / handler

* Remove test code

Remove code used for testing

* Sort usings

Remove unused using

* Fix ordering

Ordering of files when they were being processed was backwards / out of order

* Add button to "okay" all messages

PR Feedback to add a button to dismiss all of the messages all at once.
Fixing a referencing issue with `currentFile`

Co-authored-by: Michael Latouche <michael@crossingpaths.be>
This commit is contained in:
Matthew Care
2021-11-10 02:32:21 +01:00
committed by GitHub
parent 570841b958
commit fe7b696c7c
3 changed files with 139 additions and 176 deletions

View File

@@ -31,7 +31,7 @@ angular.module("umbraco.directives")
propertyAlias: '@',
accept: '@',
maxFileSize: '@',
compact: '@',
hideDropzone: '@',
acceptedMediatypes: '=',
@@ -42,9 +42,10 @@ angular.module("umbraco.directives")
},
link: function(scope, element, attrs) {
scope.queue = [];
scope.done = [];
scope.rejected = [];
scope.totalQueued = 0;
scope.currentFile = undefined;
scope.processed = [];
scope.totalMessages = 0;
function _filterFile(file) {
var ignoreFileNames = ['Thumbs.db'];
@@ -65,51 +66,50 @@ angular.module("umbraco.directives")
function _filesQueued(files, event) {
//Push into the queue
Utilities.forEach(files, file => {
if (_filterFile(file) === true) {
if (file.$error) {
scope.rejected.push(file);
} else {
scope.queue.push(file);
}
if (_filterFile(file) === true) {
file.messages = [];
scope.queue.push(file);
}
});
//when queue is done, kick the uploader
if (!scope.working) {
// Upload not allowed
if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) {
files.map(file => {
file.uploadStatus = "error";
file.serverErrorMessage = "File type is not allowed here";
scope.rejected.push(file);
});
scope.queue = [];
}
// If we have Accepted Media Types, we will ask to choose Media Type, if Choose Media Type returns false, it only had one choice and therefor no reason to
if (scope.acceptedMediatypes && _requestChooseMediaTypeDialog() === false) {
scope.contentTypeAlias = "umbracoAutoSelect";
_processQueueItem();
}
// Upload not allowed
if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) {
files.map(file => {
file.messages.push({message: "File type is not allowed here", type: "Error"});
});
}
// If we have Accepted Media Types, we will ask to choose Media Type, if Choose Media Type returns false, it only had one choice and therefor no reason to
if (scope.acceptedMediatypes && _requestChooseMediaTypeDialog() === false) {
scope.contentTypeAlias = "umbracoAutoSelect";
}
// Add the processed length, as we might be uploading in stages
scope.totalQueued = scope.queue.length + scope.processed.length;
_processQueueItems();
}
function _processQueueItem() {
if (scope.queue.length > 0) {
function _processQueueItems() {
// if we have processed all files, either by successful
// upload, or attending to all messages, we deem the
// action complete, else continue processing files
scope.totalMessages = scope.processed.filter(e => e.messages.length > 0).length;
if (scope.totalQueued === scope.processed.length) {
if (scope.totalMessages === 0) {
if (scope.filesUploaded) {
//queue is empty, trigger the done action
scope.filesUploaded(scope.done);
}
//auto-clear the done queue after 3 secs
var currentLength = scope.processed.length;
$timeout(function() {
scope.processed.splice(0, currentLength);
}, 3000);
}
} else {
scope.currentFile = scope.queue.shift();
_upload(scope.currentFile);
} else if (scope.done.length > 0) {
if (scope.filesUploaded) {
//queue is empty, trigger the done action
scope.filesUploaded(scope.done);
}
//auto-clear the done queue after 3 secs
var currentLength = scope.done.length;
$timeout(function() {
scope.done.splice(0, currentLength);
}, 3000);
}
}
@@ -134,55 +134,36 @@ angular.module("umbraco.directives")
var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10);
// set percentage property on file
file.uploadProgress = progressPercentage;
// set uploading status on file
file.uploadStatus = "uploading";
}
})
.success(function(data, status, headers, config) {
if (data.notifications && data.notifications.length > 0) {
// set error status on file
file.uploadStatus = "error";
// Throw message back to user with the cause of the error
file.serverErrorMessage = data.notifications[0].message;
// Put the file in the rejected pool
scope.rejected.push(file);
} else {
// set done status on file
file.uploadStatus = "done";
file.uploadProgress = 100;
// set date/time for when done - used for sorting
file.doneDate = new Date();
// Put the file in the done pool
scope.done.push(file);
}
.success(function (data, status, headers, config) {
// Set server messages
file.messages = data.notifications;
scope.processed.push(file);
//after processing, test if everything is done
scope.currentFile = undefined;
//after processing, test if everthing is done
_processQueueItem();
_processQueueItems();
})
.error(function(evt, status, headers, config) {
// set status done
file.uploadStatus = "error";
//if the service returns a detailed error
if (evt.InnerException) {
file.serverErrorMessage = evt.InnerException.ExceptionMessage;
file.messages.push({ message: evt.InnerException.ExceptionMessage, type: "Error" });
//Check if its the common "too large file" exception
if (evt.InnerException.StackTrace &&
evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) {
file.serverErrorMessage = "File too large to upload";
file.messages.push({ message: "File too large to upload", type: "Error" });
}
} else if (evt.Message) {
file.serverErrorMessage = evt.Message;
} else if (evt && typeof evt === 'string') {
file.serverErrorMessage = evt;
file.messages.push({message: evt.Message, type: "Error"});
} else if (evt && typeof evt === "string") {
file.messages.push({message: evt, type: "Error"});
}
// If file not found, server will return a 404 and display this message
if (status === 404) {
file.serverErrorMessage = "File not found";
file.messages.push({message: "File not found", type: "Error"});
}
//after processing, test if everthing is done
scope.rejected.push(file);
scope.currentFile = undefined;
_processQueueItem();
_processQueueItems();
});
}
@@ -224,16 +205,14 @@ angular.module("umbraco.directives")
availableItems: filteredMediaTypes,
submit: function (model) {
scope.contentTypeAlias = model.selectedItem.alias;
_processQueueItem();
_processQueueItems();
overlayService.close();
},
close: function () {
scope.queue.map(function (file) {
file.uploadStatus = "error";
file.serverErrorMessage = "No files uploaded, no mediatype selected";
scope.rejected.push(file);
file.messages.push({message:"No files uploaded, no mediatype selected", type: "Error"});
});
scope.queue = [];
@@ -245,14 +224,26 @@ angular.module("umbraco.directives")
overlayService.open(dialog);
});
return true;// yes, we did open the choose-media dialog, therefor we return true.
return true; // yes, we did open the choose-media dialog, therefore we return true.
}
scope.dismissMessages = function (file) {
file.messages = [];
_processQueueItems();
}
scope.dismissAllMessages = function () {
Utilities.forEach(scope.processed, file => {
file.messages = [];
});
_processQueueItems();
}
scope.handleFiles = function(files, event, invalidFiles) {
const allFiles = [...files, ...invalidFiles];
// add unique key for each files to use in ng-repeats
allFiles.forEach(file => {
Utilities.forEach(allFiles, file => {
file.key = String.CreateGuid();
});

View File

@@ -1,6 +1,5 @@
.umb-file-dropzone {
// drop zone
// tall and small version - animate height
.dropzone {
@@ -21,17 +20,16 @@
&.is-small {
height: 100px;
.illustration {
width: 200px;
}
}
&.drag-over {
border: 1px dashed @gray-1;
}
}
// center the content of the drop zone
.content {
position: absolute;
@@ -41,8 +39,6 @@
display: flex;
flex-direction: column;
}
// file select link
.file-select {
background: transparent;
@@ -54,11 +50,10 @@
margin-top: 10px;
&:hover {
color: @ui-action-discreet-type-hover;
text-decoration: none;
color: @ui-action-discreet-type-hover;
text-decoration: none;
}
}
// uploading / uploaded file list
.file-list {
list-style: none;
@@ -67,12 +62,10 @@
padding: 10px 20px;
.file {
//border-bottom: 1px dashed @orange;
display: block;
width: 100%;
padding: 5px 0;
position: relative;
border-top: 1px solid @gray-8;
&:first-child {
border-top: none;
}
@@ -80,13 +73,21 @@
&.ng-enter {
animation: fadeIn 0.5s;
}
&.ng-leave {
animation: fadeOut 2s;
}
.file-description {
color: @gray-3;
font-size: 12px;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.file-messages, .file-messages span {
display: block;
}
@@ -95,25 +96,11 @@
width: 100%;
}
.file-icon {
position: absolute;
right: 0;
bottom: 0;
.icon {
font-size: 20px;
&.ng-enter {
animation: fadeIn 0.5s;
}
&.ng-leave {
animation: fadeIn 0.5s;
}
}
.ok-all {
margin-left: auto;
}
}
}
// progress bars
// could be moved to its own less file
.file-progress {
@@ -131,5 +118,4 @@
width: 0;
}
}
}

View File

@@ -1,21 +1,20 @@
<div data-element="dropzone" class="umb-file-dropzone">
<ng-form name="uploadForm" umb-isolate-form>
<!-- Drag and drop files area -->
<div ngf-drop
ng-hide="hideDropzone === 'true'"
ng-model="filesHolder"
ngf-change="handleFiles($files, $event, $invalidFiles)"
class="dropzone"
ngf-drag-over-class="'drag-over'"
ngf-multiple="true"
ngf-allow-dir="true"
ngf-pattern="{{ accept }}"
ngf-max-size="{{ maxFileSize }}"
ng-class="{'is-small': compact !=='false' || (done.length + queue.length) > 0 }">
ng-hide="hideDropzone === 'true'"
ng-model="filesHolder"
ngf-change="handleFiles($files, $event, $invalidFiles)"
class="dropzone"
ngf-drag-over-class="'drag-over'"
ngf-multiple="true"
ngf-allow-dir="true"
ngf-pattern="{{ accept }}"
ngf-max-size="{{ maxFileSize }}"
ng-class="{'is-small': compact !=='false' || (processed.length + queue.length) > 0 }">
<div class="content" >
<div class="content">
<p>
<localize key="media_dragAndDropYourFilesIntoTheArea">Drag and drop your file(s) into the area</localize>
</p>
@@ -24,82 +23,69 @@
<img class="illustration" src="assets/img/uploader/upload-illustration.svg" alt="" draggable="false" />
<!-- Select files -->
<button
type="button"
data-element="button-uploadMedia"
class="file-select"
ngf-select
ng-model="filesHolder"
ngf-change="handleFiles($files, $event, $invalidFiles)"
ngf-multiple="true"
ngf-pattern="{{ accept }}"
ngf-max-size="{{ maxFileSize }}">
-&nbsp;<localize key="media_orClickHereToUpload">or click here to choose files</localize>
<button type="button"
data-element="button-uploadMedia"
class="file-select"
ngf-select
ng-model="filesHolder"
ngf-change="handleFiles($files, $event, $invalidFiles)"
ngf-multiple="true"
ngf-pattern="{{ accept }}"
ngf-max-size="{{ maxFileSize }}">
-&nbsp;<localize key="media_orClickHereToUpload">or click here to choose files</localize>
</button>
</div>
</div>
<!-- List of uploading/uploaded files -->
<ul class="file-list" ng-show="done.length > 0 || queue.length > 0 || rejected.length > 0 || filesHolder.length > 0">
<ul class="file-list" ng-show="queue.length > 0 || processed.length > 0 || filesHolder.length > 0">
<!-- make list sort order the same as photo grid. The last uploaded photo in the top -->
<li class="file" ng-repeat="file in done track by file.key">
<!-- file name -->
<div class="file-description">{{ file.name }}</div>
<li class="file" ng-if="totalMessages > 1">
<div class="file-description">
<!-- Okay all -->
<button class="btn btn-primary ok-all" type="button" ng-click="dismissAllMessages()">
<localize key="general_ok">Ok</localize> (<localize key="general_all">All</localize>)
</button>
</div>
</li>
<!-- upload success -->
<div class="file-icon" ng-if="file.uploadStatus == 'done'">
<li class="file" ng-repeat="file in processed track by file.key">
<div class="file-description">
<div>
<span>{{ file.name }}</span>
<span ng-if="file.messages.length > 0 || file.$error" class="file-messages">
<span class="errorMessage color-red" ng-repeat="message in file.messages">{{message.header}}: {{message.message}}</span>
<span ng-if="file.$error === 'pattern'" class="errorMessage color-red"><localize key="media_disallowedFileType"></localize></span>
<span ng-if="file.$error === 'maxSize'" class="errorMessage color-red"><localize key="media_maxFileSize"></localize> "{{maxFileSize}}"</span>
</span>
</div>
<!-- upload success -->
<span ng-if="file.messages.length === 0">
<umb-icon icon="icon-check" class="icon color-green"></umb-icon>
</div>
</span>
<!-- requires user input -->
<button class="btn btn-primary" type="button" ng-click="dismissMessages(file)" ng-if="file.messages.length > 0">
<localize key="general_ok">Ok</localize>
</button>
</div>
</li>
<li class="file" ng-if="currentFile">
<!-- file name -->
<div class="file-name">{{currentFile.name}}</div>
<!-- upload progress bar -->
<div class="file-name">{{currentFile.name}} {{currentFile.uploadProgress + '%'}}</div>
<div class="file-progress">
<span class="file-progress-indicator"
ng-style="{'width': currentFile.uploadProgress + '%'}"></span>
<span class="file-progress-indicator" ng-style="{'width': currentFile.uploadProgress + '%'}"></span>
</div>
</li>
<!-- make list sort order the same as photo grid. The last uploaded photo in the top -->
<li class="file" ng-repeat="queued in queue track by queued.key">
<!-- file name -->
<div class="file-name">{{queued.name}}</div>
</li>
<li class="file" ng-repeat="file in rejected track by file.key">
<!-- file name -->
<div class="file-description">
<strong>{{file.name}}</strong>
<span class="file-error" ng-if="file.$error">
<span ng-if="file.$error === 'pattern'" class="errorMessage color-red"><localize key="media_disallowedFileType">Cannot upload this file, it does not have an approved file type</localize></span>
<span ng-if="file.$error === 'maxSize'" class="errorMessage color-red"><localize key="media_maxFileSize">Max file size is</localize> "{{maxFileSize}}"</span>
</span>
<span class="file-error" ng-if="file.serverErrorMessage">
<span class="errorMessage color-red">{{file.serverErrorMessage}}</span>
</span>
</div>
<!-- upload error -->
<div class="file-icon">
<umb-icon icon="icon-delete" class="icon color-red"></umb-icon>
</div>
<li class="file" ng-repeat="file in queue track by file.key">
<div class="file-description">{{ file.name }}</div>
</li>
</ul>
</ng-form>
</div>