Fixed up the file uploading and the readonly property editors and saving/updating their data/control based on the data changed on the server side.

U4-2680 Fix file uploading
This commit is contained in:
Shannon
2013-08-21 17:54:30 +10:00
parent 466b2a1cb2
commit 5581af2d25
17 changed files with 362 additions and 146 deletions

View File

@@ -28,6 +28,19 @@ namespace Umbraco.Core
[UmbracoWillObsolete("Do not use this constants. See IShortStringHelper.CleanStringForSafeAliasJavaScriptCode.")]
public const string UmbracoInvalidFirstCharacters = "01234567890";
/// <summary>
/// This tries to detect a json string, this is not a fail safe way but it is quicker than doing
/// a try/catch when deserializing when it is not json.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
internal static bool DetectIsJson(this string input)
{
input = input.Trim();
return input.StartsWith("{") && input.EndsWith("}")
|| input.StartsWith("[") && input.EndsWith("]");
}
internal static string ReplaceNonAlphanumericChars(this string input, char replacement)
{
//any character that is not alphanumeric, convert to a hyphen

View File

@@ -94,6 +94,17 @@
return this;
};
}
if (!String.prototype.trimEnd) {
/** trims the end of the string*/
String.prototype.trimEnd = function (str) {
if (this.endsWith(str)) {
return this.substring(0, this.length - str.length);
}
return this;
};
}
if (!String.prototype.utf8Encode) {

View File

@@ -1,22 +1,24 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbFileUpload
* @function
* @restrict AE
* @element ANY
* @scope
**/
function umbFileUpload() {
return {
scope: true, //create a new scope
link: function (scope, el, attrs) {
el.bind('change', function (event) {
var files = event.target.files;
//emit event upward
scope.$emit("filesSelected", { files: files });
});
}
};
}
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbFileUpload
* @function
* @restrict A
* @scope
* @description
* Listens for file input control changes and emits events when files are selected for use in other controllers.
**/
function umbFileUpload() {
return {
restrict: "A",
scope: true, //create a new scope
link: function (scope, el, attrs) {
el.bind('change', function (event) {
var files = event.target.files;
//emit event upward
scope.$emit("filesSelected", { files: files });
});
}
};
}
angular.module('umbraco.directives').directive("umbFileUpload", umbFileUpload);

View File

@@ -0,0 +1,33 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbFileUpload
* @function
* @restrict A
* @scope
* @description
* A single file upload field that will reset itself based on the object passed in for the rebuild parameter. This
* is required because the only way to reset an upload control is to replace it's html.
**/
function umbSingleFileUpload($compile) {
return {
restrict: "E",
scope: {
rebuild: "="
},
replace: true,
template: "<div><input type='file' umb-file-upload /></div>",
link: function (scope, el, attrs) {
scope.$watch("rebuild", function (newVal, oldVal) {
if (newVal && newVal !== oldVal) {
//recompile it!
el.html("<input type='file' umb-file-upload />");
$compile(el.contents())(scope);
}
});
}
};
}
angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload);

View File

@@ -38,10 +38,14 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser
* @description
* re-binds all changed property values to the origContent object from the newContent object and returns an array of changed properties.
*/
reBindChangedProperties: function (allOrigProps, allNewProps) {
reBindChangedProperties: function (origContent, newContent) {
var changed = [];
//get a list of properties since they are contained in tabs
var allOrigProps = this.getAllProps(origContent);
var allNewProps = this.getAllProps(newContent);
function getNewProp(alias) {
if (alias.startsWith("_umb_")) {
return null;
@@ -54,7 +58,7 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser
for (var p in allOrigProps) {
var newProp = getNewProp(allOrigProps[p].alias);
if (newProp && !_.isEqual(allOrigProps[p].value, newProp.value)) {
//they have changed so set the origContent prop's value to the new value
//they have changed so set the origContent prop to the new one
allOrigProps[p].value = newProp.value;
changed.push(allOrigProps[p]);
}
@@ -134,9 +138,6 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser
if (!args.allNewProps && !angular.isArray(args.allNewProps)) {
throw "args.allNewProps must be a valid array";
}
if (!args.allOrigProps && !angular.isArray(args.allOrigProps)) {
throw "args.allOrigProps must be a valid array";
}
//When the status is a 403 status, we have validation errors.
//Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).

View File

@@ -0,0 +1,64 @@
/**
* @ngdoc service
* @name umbraco.services.fileManager
* @function
*
* @description
* Used by editors to manage any files that require uploading with the posted data, normally called by property editors
* that need to attach files.
*/
function fileManager() {
var fileCollection = [];
return {
/**
* @ngdoc function
* @name umbraco.services.fileManager#addFiles
* @methodOf umbraco.services.fileManager
* @function
*
* @description
* Attaches files to the current manager for the current editor for a particular property, if an empty array is set
* for the files collection that effectively clears the files for the specified editor.
*/
setFiles: function(propertyId, files) {
//this will clear the files for the current property and then add the new ones for the current property
fileCollection = _.reject(fileCollection, function (item) {
return item.id === propertyId;
});
for (var i = 0; i < files.length; i++) {
//save the file object to the files collection
fileCollection.push({ id: propertyId, file: files[i] });
}
},
/**
* @ngdoc function
* @name umbraco.services.fileManager#getFiles
* @methodOf umbraco.services.fileManager
* @function
*
* @description
* Returns all of the files attached to the file manager
*/
getFiles: function() {
return fileCollection;
},
/**
* @ngdoc function
* @name umbraco.services.fileManager#clearFiles
* @methodOf umbraco.services.fileManager
* @function
*
* @description
* Removes all files from the manager
*/
clearFiles: function () {
fileCollection = [];
}
};
}
angular.module('umbraco.services').factory('fileManager', fileManager);

View File

@@ -63,7 +63,7 @@ function MainController($scope, $routeParams, $rootScope, $timeout, $http, notif
if($scope.user.avatar){
$http.get($scope.user.avatar).then(function(){
alert($scope.user.avatar);
//alert($scope.user.avatar);
$scope.avatar = $scope.user.avatar;
});
}

View File

@@ -6,8 +6,11 @@
* @description
* The controller for the content editor
*/
function ContentEditController($scope, $routeParams, $location, contentResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper) {
function ContentEditController($scope, $routeParams, $location, contentResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager) {
//initialize the file manager
fileManager.clearFiles();
if ($routeParams.create) {
//we are creating so get an empty content item
contentResource.getScaffold($routeParams.id, $routeParams.doctype)
@@ -31,18 +34,6 @@ function ContentEditController($scope, $routeParams, $location, contentResource,
});
}
$scope.files = [];
$scope.addFiles = function (propertyId, files) {
//this will clear the files for the current property and then add the new ones for the current property
$scope.files = _.reject($scope.files, function (item) {
return item.id == propertyId;
});
for (var i = 0; i < files.length; i++) {
//save the file object to the scope's files collection
$scope.files.push({ id: propertyId, file: files[i] });
}
};
//TODO: Need to figure out a way to share the saving and event broadcasting with all editors!
$scope.saveAndPublish = function () {
@@ -55,7 +46,7 @@ function ContentEditController($scope, $routeParams, $location, contentResource,
serverValidationManager.reset();
contentResource.publish($scope.content, $routeParams.create, $scope.files)
contentResource.publish($scope.content, $routeParams.create, fileManager.getFiles())
.then(function (data) {
contentEditingHelper.handleSuccessfulSave({
@@ -91,7 +82,7 @@ function ContentEditController($scope, $routeParams, $location, contentResource,
serverValidationManager.reset();
contentResource.save($scope.content, $routeParams.create, $scope.files)
contentResource.save($scope.content, $routeParams.create, fileManager.getFiles())
.then(function (data) {
contentEditingHelper.handleSuccessfulSave({

View File

@@ -6,7 +6,10 @@
* @description
* The controller for the media editor
*/
function mediaEditController($scope, $routeParams, mediaResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper) {
function mediaEditController($scope, $routeParams, mediaResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager) {
//initialize the file manager
fileManager.clearFiles();
if ($routeParams.create) {
@@ -30,18 +33,6 @@ function mediaEditController($scope, $routeParams, mediaResource, notificationsS
});
}
$scope.files = [];
$scope.addFiles = function (propertyId, files) {
//this will clear the files for the current property and then add the new ones for the current property
$scope.files = _.reject($scope.files, function (item) {
return item.id == propertyId;
});
for (var i = 0; i < files.length; i++) {
//save the file object to the scope's files collection
$scope.files.push({ id: propertyId, file: files[i] });
}
};
$scope.save = function () {
@@ -53,28 +44,22 @@ function mediaEditController($scope, $routeParams, mediaResource, notificationsS
serverValidationManager.reset();
mediaResource.save($scope.content, $routeParams.create, $scope.files)
mediaResource.save($scope.content, $routeParams.create, fileManager.getFiles())
.then(function (data) {
contentEditingHelper.handleSuccessfulSave({
scope: $scope,
newContent: data,
rebindCallback: contentEditingHelper.reBindChangedProperties(
contentEditingHelper.getAllProps($scope.content),
contentEditingHelper.getAllProps(data))
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data)
});
}, function (err) {
var allNewProps = contentEditingHelper.getAllProps(err.data);
var allOrigProps = contentEditingHelper.getAllProps($scope.content);
contentEditingHelper.handleSaveError({
err: err,
redirectOnFailure: true,
allNewProps: allNewProps,
allOrigProps: contentEditingHelper.getAllProps($scope.content),
rebindCallback: contentEditingHelper.reBindChangedProperties(allOrigProps, allNewProps)
allNewProps: contentEditingHelper.getAllProps(err.data),
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data)
});
});
};

View File

@@ -4,41 +4,64 @@
* @function
*
* @description
* The controller for the file upload property editor
* The controller for the file upload property editor. It is important to note that the $scope.model.value
* doesn't necessarily depict what is saved for this property editor. $scope.model.value can be empty when we
* are submitting files because in that case, we are adding files to the fileManager which is what gets peristed
* on the server. However, when we are clearing files, we are setting $scope.model.value to "{clearFiles: true}"
* to indicate on the server that we are removing files for this property. We will keep the $scope.model.value to
* be the name of the file selected (if it is a newly selected file) or keep it to be it's original value, this allows
* for the editors to check if the value has changed and to re-bind the property if that is true.
*
*/
function fileUploadController($scope, $element, $compile, umbImageHelper) {
function fileUploadController($scope, $element, $compile, umbImageHelper, fileManager) {
/** Clears the file collections when content is saving (if we need to clear) or after saved */
function clearFiles() {
//TODO: There should be a better way! We don't want to have to know about the parent scope
//clear the parent files collection (we don't want to upload any!)
$scope.$parent.addFiles($scope.id, []);
function clearFiles() {
//clear the files collection (we don't want to upload any!)
fileManager.setFiles($scope.id, []);
//clear the current files
$scope.files = [];
}
//clear the current files
$scope.files = [];
//create the property to show the list of files currently saved
if ($scope.model.value != "") {
var images = $scope.model.value.split(",");
/** this method is used to initialize the data and to re-initialize it if the server value is changed */
function initialize(index)
{
if (!index) {
index = 1;
}
$scope.persistedFiles = _.map(images, function (item) {
return { file: item, isImage: umbImageHelper.detectIfImageByExtension(item) };
//this is used in order to tell the umb-single-file-upload directive to
//rebuild the html input control (and thus clearing the selected file) since
//that is the only way to manipulate the html for the file input control.
$scope.rebuildInput = {
index: index
};
//clear the current files
$scope.files = [];
//store the original value so we can restore it if the user clears and then cancels clearing.
$scope.originalValue = $scope.model.value;
//create the property to show the list of files currently saved
if ($scope.model.value != "") {
var images = $scope.model.value.split(",");
$scope.persistedFiles = _.map(images, function (item) {
return { file: item, isImage: umbImageHelper.detectIfImageByExtension(item) };
});
}
else {
$scope.persistedFiles = [];
}
_.each($scope.persistedFiles, function (file) {
file.thumbnail = umbImageHelper.getThumbnailFromPath(file.file);
});
$scope.clearFiles = false;
}
else {
$scope.persistedFiles = [];
}
_.each($scope.persistedFiles, function (file) {
file.thumbnail = umbImageHelper.getThumbnailFromPath(file.file);
});
$scope.clearFiles = false;
initialize();
//listen for clear files changes to set our model to be sent up to the server
$scope.$watch("clearFiles", function (isCleared) {
@@ -47,25 +70,48 @@ function fileUploadController($scope, $element, $compile, umbImageHelper) {
clearFiles();
}
else {
$scope.model.value = "";
//reset to original value
$scope.model.value = $scope.originalValue;
}
});
//listen for when a file is selected
$scope.$on("filesSelected", function (event, args) {
$scope.$apply(function () {
//set the parent files collection
$scope.$parent.addFiles($scope.model.id, args.files);
//set the files collection
fileManager.setFiles($scope.model.id, args.files);
//clear the current files
$scope.files = [];
var newVal = "";
for (var i = 0; i < args.files.length; i++) {
//save the file object to the scope's files collection
$scope.files.push({ id: $scope.model.id, file: args.files[i] });
newVal += args.files[i].name + ",";
}
//set clear files to false, this will reset the model too
$scope.clearFiles = false;
//set the model value to be the concatenation of files selected. Please see the notes
// in the description of this controller, it states that this value isn't actually used for persistence,
// but we need to set it to something so that the editor and the server can detect that it's been changed.
$scope.model.value = "{selectedFiles: '" + newVal.trimEnd(",") + "'}";
});
});
//listen for when the model value has changed
$scope.$watch("model.value", function(newVal, oldVal) {
//cannot just check for !newVal because it might be an empty string which we
//want to look for.
if (newVal !== null && newVal !== undefined && newVal !== oldVal) {
//now we need to check if we need to re-initialize our structure which is kind of tricky
// since we only want to do that if the server has changed the value, not if this controller
// has changed the value. There's only 2 scenarios where we change the value internall so
// we know what those values can be, if they are not either of them, then we'll re-initialize.
if (newVal !== "{clearFiles: true}" && newVal !== $scope.originalValue && !newVal.startsWith("{selectedFiles:")) {
initialize($scope.rebuildInput.index + 1);
}
}
});
};
angular.module("umbraco").controller('Umbraco.Editors.FileUploadController', fileUploadController);

View File

@@ -2,7 +2,7 @@
<div ng-hide="clearFiles">
<div class="file-uploader">
<input type="file" umb-file-upload multiple />
<umb-single-file-upload rebuild="rebuildInput"></umb-single-file-upload>
</div>
<ul ng-show="files.length > 0">
<li ng-repeat="file in files">{{file.file.name}}</li>
@@ -18,7 +18,7 @@
</div>
</li>
</ul>
<input type="checkbox" name="myPackage_clearFiles" ng-model="clearFiles" />
<label for="myPackage_clearFiles">Clear files</label>
<input type="checkbox" id="clearFiles-{{model.id}}" name="clearFiles" ng-model="clearFiles" />
<label for="clearFiles-{{model.id}}">Clear files</label>
</div>
</div>

View File

@@ -1,21 +1,48 @@
angular.module('umbraco').controller("Umbraco.Editors.ReadOnlyValueController",
function($rootScope, $scope, $filter){
/**
* @ngdoc controller
* @name Umbraco.Editors.ReadOnlyValueController
* @function
*
* @description
* The controller for the readonlyvalue property editor.
* This controller offer more functionality than just a simple label as it will be able to apply formatting to the
* value to be displayed. This means that we also have to apply more complex logic of watching the model value when
* it changes because we are creating a new scope value called displayvalue which will never change based on the server data.
* In some cases after a form submission, the server will modify the data that has been persisted, especially in the cases of
* readonlyvalues so we need to ensure that after the form is submitted that the new data is reflected here.
*/
function ReadOnlyValueController($rootScope, $scope, $filter) {
if ($scope.model.config &&
angular.isArray($scope.model.config) &&
$scope.model.config.length > 0 &&
$scope.model.config[0] &&
$scope.model.config.filter)
{
if ($scope.model.config.format) {
$scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value, $scope.model.config.format);
}
else {
$scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value);
}
}
else {
$scope.displayvalue = $scope.model.value;
}
});
function formatDisplayValue() {
if ($scope.model.config &&
angular.isArray($scope.model.config) &&
$scope.model.config.length > 0 &&
$scope.model.config[0] &&
$scope.model.config.filter) {
if ($scope.model.config.format) {
$scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value, $scope.model.config.format);
} else {
$scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value);
}
} else {
$scope.displayvalue = $scope.model.value;
}
}
//format the display value on init:
formatDisplayValue();
$scope.$watch("model.value", function (newVal, oldVal) {
//cannot just check for !newVal because it might be an empty string which we
//want to look for.
if (newVal !== null && newVal !== undefined && newVal !== oldVal) {
//update the display val again
formatDisplayValue();
}
});
}
angular.module('umbraco').controller("Umbraco.Editors.ReadOnlyValueController", ReadOnlyValueController);

View File

@@ -36,23 +36,7 @@ describe('edit content controller tests', function () {
}));
describe('content edit controller save and publish', function () {
it('it should define the default properties on construction', function () {
expect(scope.files).toNotBe(undefined);
});
it('adding a file adds to the collection', function () {
scope.addFiles(123, ["testFile"]);
expect(scope.files.length).toBe(1);
});
it('adding a file with the same property id replaces the existing one', function () {
scope.addFiles(123, ["testFile"]);
scope.addFiles(123, ["testFile2"]);
expect(scope.files.length).toBe(1);
expect(scope.files[0].file).toBe("testFile2");
});
it('it should have an content object', function() {
//controller should have a content object

View File

@@ -218,9 +218,7 @@ describe('contentEditingHelper tests', function () {
newContent.tabs[1].properties[2].value = "origValue4";
//act
var changed = contentEditingHelper.reBindChangedProperties(
contentEditingHelper.getAllProps(origContent),
contentEditingHelper.getAllProps(newContent));
var changed = contentEditingHelper.reBindChangedProperties(origContent, newContent);
//assert
expect(changed.length).toBe(2);

View File

@@ -0,0 +1,33 @@
describe('file manager tests', function () {
var fileManager;
beforeEach(module('umbraco.services'));
beforeEach(inject(function ($injector) {
fileManager = $injector.get('fileManager');
}));
describe('file management', function () {
it('adding a file adds to the collection', function () {
fileManager.setFiles(123, ["testFile"]);
expect(fileManager.getFiles().length).toBe(1);
});
it('adding a file with the same property id replaces the existing one', function () {
fileManager.setFiles(123, ["testFile"]);
fileManager.setFiles(123, ["testFile2"]);
expect(fileManager.getFiles().length).toBe(1);
expect(fileManager.getFiles()[0].file).toBe("testFile2");
});
it('clears all files', function () {
fileManager.setFiles(123, ["testFile"]);
fileManager.setFiles(234, ["testFile"]);
expect(fileManager.getFiles().length).toBe(2);
fileManager.clearFiles();
expect(fileManager.getFiles().length).toBe(0);
});
});
});

View File

@@ -66,20 +66,39 @@ namespace Umbraco.Web.PropertyEditors
UmbracoSettings.ImageAutoFillImageProperties.SelectSingleNode(
string.Format("uploadField [@alias = \"{0}\"]", p.Alias));
if (uploadFieldConfigNode != null && p.Value != null && p.Value is string && ((string)p.Value).IsNullOrWhiteSpace() == false)
if (uploadFieldConfigNode != null)
{
//there might be multiple, we can only process the first one!
var split = ((string) p.Value).Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
if (split.Any())
//now we need to check if there is a value
if (p.Value is string && ((string) p.Value).IsNullOrWhiteSpace() == false)
{
var umbracoFile = new UmbracoMediaFile(IOHelper.MapPath(split[0]));
FillProperties(uploadFieldConfigNode, model, umbracoFile);
//there might be multiple, we can only process the first one!
var split = ((string) p.Value).Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
if (split.Any())
{
var umbracoFile = new UmbracoMediaFile(IOHelper.MapPath(split[0]));
FillProperties(uploadFieldConfigNode, model, umbracoFile);
}
}
else
{
//there's no value so need to reset to zero
ResetProperties(uploadFieldConfigNode, model);
}
}
}
}
}
private static void ResetProperties(XmlNode uploadFieldConfigNode, IContentBase content)
{
// only add dimensions to web images
UpdateContentProperty(uploadFieldConfigNode, content, "widthFieldAlias", string.Empty);
UpdateContentProperty(uploadFieldConfigNode, content, "heightFieldAlias", string.Empty);
UpdateContentProperty(uploadFieldConfigNode, content, "lengthFieldAlias", string.Empty);
UpdateContentProperty(uploadFieldConfigNode, content, "extensionFieldAlias", string.Empty);
}
private static void FillProperties(XmlNode uploadFieldConfigNode, IContentBase content, UmbracoMediaFile um)
{
// only add dimensions to web images

View File

@@ -28,7 +28,12 @@ namespace Umbraco.Web.PropertyEditors
/// <summary>
/// Overrides the deserialize value so that we can save the file accordingly
/// </summary>
/// <param name="editorValue"></param>
/// <param name="editorValue">
/// This is value passed in from the editor. We normally don't care what the editorValue.Value is set to because
/// we are more interested in the files collection associated with it, however we do care about the value if we
/// are clearing files. By default the editorValue.Value will just be set to the name of the file (but again, we
/// just ignore this and deal with the file collection in editorValue.AdditionalData.ContainsKey("files") )
/// </param>
/// <param name="currentValue">
/// The current value persisted for this property. This will allow us to determine if we want to create a new
/// file path or use the existing file path.
@@ -36,16 +41,20 @@ namespace Umbraco.Web.PropertyEditors
/// <returns></returns>
public override object DeserializeValue(ContentPropertyData editorValue, object currentValue)
{
if (currentValue == null)
{
currentValue = string.Empty;
}
//if the value is the same then just return the current value
if (currentValue != null && editorValue.Value == currentValue.ToString())
//if the value is the same then just return the current value so we don't re-process everything
if (string.IsNullOrEmpty(currentValue.ToString()) == false && editorValue.Value == currentValue.ToString())
{
return currentValue;
}
//check the editorValue value to see if we need to clear the files or not
//check the editorValue value to see if we need to clear the files or not.
var clear = false;
if (editorValue.Value.IsNullOrWhiteSpace() == false)
if (editorValue.Value.IsNullOrWhiteSpace() == false && editorValue.Value.StartsWith("{clearFiles:"))
{
try
{
@@ -59,7 +68,7 @@ namespace Umbraco.Web.PropertyEditors
}
var currentPersistedValues = new string[] {};
if (currentValue != null)
if (string.IsNullOrEmpty(currentValue.ToString()) == false)
{
currentPersistedValues = currentValue.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
}