diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 73b5ed34db..84cf2869e7 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -9,6 +9,8 @@ using System.Linq; using System.Web; using System.Xml; using System.Xml.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; @@ -539,7 +541,7 @@ namespace Umbraco.Core.Models return ApplicationContext.Current.Services.ContentService.HasPublishedVersion(content.Id); } - + #region Tag methods ///// @@ -567,6 +569,21 @@ namespace Umbraco.Core.Models /// The group/category to assign the tags, the default value is "default" /// public static void SetTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool replaceTags, string tagGroup = "default") + { + content.SetTags(TagCacheStorageType.Csv, propertyTypeAlias, tags, replaceTags, tagGroup); + } + + /// + /// Sets tags for the property - will add tags to the tags table and set the property value to be the comma delimited value of the tags. + /// + /// The content item to assign the tags to + /// The tag storage type in cache (default is csv) + /// The property alias to assign the tags to + /// The tags to assign + /// True to replace the tags on the current property with the tags specified or false to merge them with the currently assigned ones + /// The group/category to assign the tags, the default value is "default" + /// + public static void SetTags(this IContentBase content, TagCacheStorageType storageType, string propertyTypeAlias, IEnumerable tags, bool replaceTags, string tagGroup = "default") { var property = content.Properties[propertyTypeAlias]; if (property == null) @@ -583,15 +600,39 @@ namespace Umbraco.Core.Models //ensure the property value is set to the same thing if (replaceTags) { - property.Value = string.Join(",", trimmedTags); + switch (storageType) + { + case TagCacheStorageType.Csv: + property.Value = string.Join(",", trimmedTags); + break; + case TagCacheStorageType.Json: + //json array + property.Value = JsonConvert.SerializeObject(trimmedTags); + break; + } + } else { - var currTags = property.Value.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) + switch (storageType) + { + case TagCacheStorageType.Csv: + var currTags = property.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()); - property.Value = string.Join(",", trimmedTags.Union(currTags)); + property.Value = string.Join(",", trimmedTags.Union(currTags)); + break; + case TagCacheStorageType.Json: + var currJson = JsonConvert.DeserializeObject(property.Value.ToString()); + //need to append the new ones + foreach (var tag in trimmedTags) + { + currJson.Add(tag); + } + //json array + property.Value = JsonConvert.SerializeObject(currJson); + break; + } } - } /// @@ -664,7 +705,7 @@ namespace Umbraco.Core.Models { return ApplicationContext.Current.Services.PackagingService.Export(media, true, raiseEvents: false); } - + /// /// Creates the xml representation for the object /// @@ -687,10 +728,10 @@ namespace Umbraco.Core.Models { return ((PackagingService)(ApplicationContext.Current.Services.PackagingService)).Export(member); } - + #endregion } - + } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PropertyTagBehavior.cs b/src/Umbraco.Core/Models/PropertyTagBehavior.cs new file mode 100644 index 0000000000..0a435ebf95 --- /dev/null +++ b/src/Umbraco.Core/Models/PropertyTagBehavior.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Models +{ + internal enum PropertyTagBehavior + { + Replace, + Remove, + Merge + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PropertyTags.cs b/src/Umbraco.Core/Models/PropertyTags.cs index d64a1c4edf..6075502131 100644 --- a/src/Umbraco.Core/Models/PropertyTags.cs +++ b/src/Umbraco.Core/Models/PropertyTags.cs @@ -33,11 +33,4 @@ namespace Umbraco.Core.Models public IEnumerable> Tags { get; set; } } - - internal enum PropertyTagBehavior - { - Replace, - Remove, - Merge - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/TagCacheStorageType.cs b/src/Umbraco.Core/Models/TagCacheStorageType.cs new file mode 100644 index 0000000000..5078a44d85 --- /dev/null +++ b/src/Umbraco.Core/Models/TagCacheStorageType.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Models +{ + public enum TagCacheStorageType + { + Csv, + Json + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs index 4ef33b7f4c..44d6a6f369 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs @@ -185,10 +185,6 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null) { - //ensure that we html encode any comma's so they are found! - // http://issues.umbraco.org/issue/U4-4741 - var replaced = tag.Replace(",", ","); - var nodeObjectType = GetNodeObjectType(objectType); var sql = new Sql() @@ -203,7 +199,7 @@ namespace Umbraco.Core.Persistence.Repositories .InnerJoin() .On(left => left.NodeId, right => right.NodeId) .Where(dto => dto.NodeObjectType == nodeObjectType) - .Where(dto => dto.Tag == replaced); + .Where(dto => dto.Tag == tag); if (tagGroup.IsNullOrWhiteSpace() == false) { @@ -289,7 +285,7 @@ namespace Umbraco.Core.Persistence.Repositories if (group.IsNullOrWhiteSpace() == false) { sql = sql.Where(dto => dto.Group == group); - } + } var factory = new TagFactory(); @@ -333,7 +329,7 @@ namespace Umbraco.Core.Persistence.Repositories //NOTE: There's some very clever logic in the umbraco.cms.businesslogic.Tags.Tag to insert tags where they don't exist, // and assign where they don't exist which we've borrowed here. The queries are pretty zany but work, otherwise we'll end up // with quite a few additional queries. - + //do all this in one transaction using (var trans = Database.GetTransaction()) { @@ -401,7 +397,7 @@ namespace Umbraco.Core.Persistence.Repositories public void RemoveTagsFromProperty(int contentId, int propertyTypeId, IEnumerable tags) { var tagSetSql = GetTagSet(tags); - + var deleteSql = string.Concat("DELETE FROM cmsTagRelationship WHERE nodeId = ", contentId, " AND propertyTypeId = ", @@ -446,11 +442,7 @@ namespace Umbraco.Core.Persistence.Repositories var array = tagsToInsert .Select(tag => string.Format("select '{0}' as Tag, '{1}' as [Group]", - PetaPocoExtensions.EscapeAtSymbols( - tag.Text - .Replace("'", "''") //NOTE: I'm not sure about this apostrophe replacement but it's been like that for a long time - .Replace(",", ",")), //NOTE: We need to replace commas with html encoded ones: http://issues.umbraco.org/issue/U4-4741 - tag.Group)) + PetaPocoExtensions.EscapeAtSymbols(tag.Text.Replace("'", "''")), tag.Group)) .ToArray(); return "(" + string.Join(" union ", array).Replace(" ", " ") + ") as TagSet"; } diff --git a/src/Umbraco.Core/PropertyEditors/SupportTagsAttribute.cs b/src/Umbraco.Core/PropertyEditors/SupportTagsAttribute.cs index 60f32c086d..3ff124d345 100644 --- a/src/Umbraco.Core/PropertyEditors/SupportTagsAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/SupportTagsAttribute.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Core.Models; namespace Umbraco.Core.PropertyEditors { @@ -30,6 +31,7 @@ namespace Umbraco.Core.PropertyEditors Delimiter = ","; ReplaceTags = true; TagGroup = "default"; + StorageType = TagCacheStorageType.Csv; } /// @@ -37,6 +39,11 @@ namespace Umbraco.Core.PropertyEditors /// public TagValueType ValueType { get; set; } + /// + /// Defines how to store the tags in cache (CSV or Json) + /// + public TagCacheStorageType StorageType { get; set; } + /// /// Defines a custom delimiter, the default is a comma /// diff --git a/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs b/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs index 743cd08517..978a7bf2ba 100644 --- a/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs +++ b/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models.Editors; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; namespace Umbraco.Core.PropertyEditors { @@ -30,8 +31,14 @@ namespace Umbraco.Core.PropertyEditors Delimiter = tagsAttribute.Delimiter; ReplaceTags = tagsAttribute.ReplaceTags; TagGroup = tagsAttribute.TagGroup; + StorageType = TagCacheStorageType.Csv; } + /// + /// Defines how to store the tags in cache (CSV or Json) + /// + public virtual TagCacheStorageType StorageType { get; private set; } + /// /// Defines a custom delimiter, the default is a comma /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8b484914ec..6931fa1c84 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -349,12 +349,14 @@ + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index 3c338b0dde..be97b0b962 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -12,15 +12,21 @@ angular.module("umbraco") //load current value $scope.currentTags = []; if ($scope.model.value) { - $scope.currentTags = $scope.model.value.split(","); + if ($scope.model.config.storageType && $scope.model.config.storageType === "Json") { + //it's a json array already + $scope.currentTags = $scope.model.value; + } + else { + //it is csv + $scope.currentTags = $scope.model.value.split(","); + } + } //Helper method to add a tag on enter or on typeahead select function addTag(tagToAdd) { if (tagToAdd.length > 0) { - if ($scope.currentTags.indexOf(tagToAdd) < 0) { - //we need to html encode any tag containing commas: http://issues.umbraco.org/issue/U4-4741 - tagToAdd = tagToAdd.replace(/\,/g, ","); + if ($scope.currentTags.indexOf(tagToAdd) < 0) { $scope.currentTags.push(tagToAdd); } } @@ -49,16 +55,24 @@ angular.module("umbraco") } }; - //sync model on submit (needed since we convert an array to string) + //sync model on submit, always push up a json array $scope.$on("formSubmitting", function (ev, args) { - $scope.model.value = $scope.currentTags.join(); + $scope.model.value = $scope.currentTags; }); //vice versa $scope.model.onValueChanged = function (newVal, oldVal) { //update the display val again if it has changed from the server - $scope.model.val = newVal; - $scope.currentTags = $scope.model.value.split(","); + $scope.model.value = newVal; + + if ($scope.model.config.storageType && $scope.model.config.storageType === "Json") { + //it's a json array already + $scope.currentTags = $scope.model.value; + } + else { + //it is csv + $scope.currentTags = $scope.model.value.split(","); + } }; //configure the tags data source diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.prevalues.html new file mode 100644 index 0000000000..2eb9aecf1e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.prevalues.html @@ -0,0 +1,10 @@ +
+ + + +
\ No newline at end of file diff --git a/src/Umbraco.Web/Editors/TagExtractor.cs b/src/Umbraco.Web/Editors/TagExtractor.cs index f0de24d6e8..361e7700c9 100644 --- a/src/Umbraco.Web/Editors/TagExtractor.cs +++ b/src/Umbraco.Web/Editors/TagExtractor.cs @@ -43,15 +43,15 @@ namespace Umbraco.Web.Editors LogHelper.Error("Could not create custom " + attribute.TagPropertyDefinitionType + " tag definition", ex); throw; } - SetPropertyTags(content, property, convertedPropertyValue, def.Delimiter, def.ReplaceTags, def.TagGroup, attribute.ValueType); + SetPropertyTags(content, property, convertedPropertyValue, def.Delimiter, def.ReplaceTags, def.TagGroup, attribute.ValueType, def.StorageType); } else { - SetPropertyTags(content, property, convertedPropertyValue, attribute.Delimiter, attribute.ReplaceTags, attribute.TagGroup, attribute.ValueType); + SetPropertyTags(content, property, convertedPropertyValue, attribute.Delimiter, attribute.ReplaceTags, attribute.TagGroup, attribute.ValueType, attribute.StorageType); } } - public static void SetPropertyTags(IContentBase content, Property property, object convertedPropertyValue, string delimiter, bool replaceTags, string tagGroup, TagValueType valueType) + public static void SetPropertyTags(IContentBase content, Property property, object convertedPropertyValue, string delimiter, bool replaceTags, string tagGroup, TagValueType valueType, TagCacheStorageType storageType) { if (convertedPropertyValue == null) { @@ -62,14 +62,14 @@ namespace Umbraco.Web.Editors { case TagValueType.FromDelimitedValue: var tags = convertedPropertyValue.ToString().Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - content.SetTags(property.Alias, tags, replaceTags, tagGroup); + content.SetTags(storageType, property.Alias, tags, replaceTags, tagGroup); break; case TagValueType.CustomTagList: //for this to work the object value must be IENumerable var stringList = convertedPropertyValue as IEnumerable; if (stringList != null) { - content.SetTags(property.Alias, stringList, replaceTags, tagGroup); + content.SetTags(storageType, property.Alias, stringList, replaceTags, tagGroup); } break; } diff --git a/src/Umbraco.Web/PropertyEditors/TagPropertyEditorTagDefinition.cs b/src/Umbraco.Web/PropertyEditors/TagPropertyEditorTagDefinition.cs index 2754af85f4..8e721366b8 100644 --- a/src/Umbraco.Web/PropertyEditors/TagPropertyEditorTagDefinition.cs +++ b/src/Umbraco.Web/PropertyEditors/TagPropertyEditorTagDefinition.cs @@ -1,10 +1,13 @@ -using Umbraco.Core.Models.Editors; +using System; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { /// - /// Used to dynamically change the tag group based on the pre-values + /// Used to dynamically change the tag group and storage type based on the pre-values /// internal class TagPropertyEditorTagDefinition : TagPropertyDefinition { @@ -21,5 +24,16 @@ namespace Umbraco.Web.PropertyEditors return preVals.ContainsKey("group") ? preVals["group"].Value : "default"; } } + + public override TagCacheStorageType StorageType + { + get + { + var preVals = PropertySaving.PreValues.FormatAsDictionary(); + return preVals.ContainsKey("storageType") + ? Enum.Parse(preVals["storageType"].Value) + : TagCacheStorageType.Csv; + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index dc42b74e0c..49fb5ddbc4 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -1,10 +1,15 @@ using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Newtonsoft.Json.Linq; using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [SupportTags(typeof(TagPropertyEditorTagDefinition))] + [SupportTags(typeof(TagPropertyEditorTagDefinition), ValueType = TagValueType.CustomTagList)] [PropertyEditor(Constants.PropertyEditors.TagsAlias, "Tags", "tags")] public class TagsPropertyEditor : PropertyEditor { @@ -12,7 +17,8 @@ namespace Umbraco.Web.PropertyEditors { _defaultPreVals = new Dictionary { - {"group", "default"} + {"group", "default"}, + {"storageType", TagCacheStorageType.Csv.ToString()} }; } @@ -27,11 +33,36 @@ namespace Umbraco.Web.PropertyEditors set { _defaultPreVals = value; } } + protected override PropertyValueEditor CreateValueEditor() + { + return new TagPropertyValueEditor(base.CreateValueEditor()); + } + protected override PreValueEditor CreatePreValueEditor() { return new TagPreValueEditor(); } + internal class TagPropertyValueEditor : PropertyValueEditorWrapper + { + public TagPropertyValueEditor(PropertyValueEditor wrapped) + : base(wrapped) + { + } + + /// + /// This needs to return IEnumerable{string} + /// + /// + /// + /// + public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) + { + var json = editorValue.Value as JArray; + return json == null ? null : json.Select(x => x.Value()); + } + } + internal class TagPreValueEditor : PreValueEditor { public TagPreValueEditor() @@ -43,11 +74,26 @@ namespace Umbraco.Web.PropertyEditors Name = "Tag group", View = "requiredfield" }); + + Fields.Add(new PreValueField(new ManifestPropertyValidator {Type = "Required"}) + { + Description = "Select whether to store the tags in cache as CSV (default) or as JSON. The only benefits of storage as JSON is that you are able to have commas in a tag value but this will require parsing the json in your views or using a property value converter", + Key = "storageType", + Name = "Storage Type", + View = "views/propertyeditors/tags/tags.prevalues.html" + }); } - public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, Core.Models.PreValueCollection persistedPreVals) + public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) { var result = base.ConvertDbToEditor(defaultPreVals, persistedPreVals); + + //This is required because we've added this pre-value so old installs that don't have it will need to have a default. + if (result.ContainsKey("storageType") == false || result["storageType"] == null || result["storageType"].ToString().IsNullOrWhiteSpace()) + { + result["storageType"] = TagCacheStorageType.Csv.ToString(); + } + return result; } }