diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index c52c8831e0..cf4958bc98 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -27,7 +27,7 @@ - + diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index 2d4042fad0..b53a2b8eaf 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -77,7 +77,7 @@ /// alias for the macro tree. /// public const string Macros = "macros"; - + /// /// alias for the datatype tree. /// @@ -92,7 +92,7 @@ /// alias for the dictionary tree. /// public const string Dictionary = "dictionary"; - + public const string Stylesheets = "stylesheets"; /// @@ -121,7 +121,7 @@ public const string Templates = "templates"; public const string RelationTypes = "relationTypes"; - + public const string Languages = "languages"; /// diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index 5cc818a6d2..d8283fd112 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -50,8 +50,6 @@ namespace Umbraco.Core.Migrations.Install typeof (MemberTypeDto), typeof (MemberDto), typeof (Member2MemberGroupDto), - typeof (ContentXmlDto), - typeof (PreviewXmlDto), typeof (PropertyTypeGroupDto), typeof (PropertyTypeDto), typeof (PropertyDataDto), @@ -139,13 +137,18 @@ namespace Umbraco.Core.Migrations.Install /// Validates the schema of the current database. /// internal DatabaseSchemaResult ValidateSchema() + { + return ValidateSchema(OrderedTables); + } + + internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) { var result = new DatabaseSchemaResult(SqlSyntax); result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) .Select(x => new DbIndexDefinition(x))); - result.TableDefinitions.AddRange(OrderedTables + result.TableDefinitions.AddRange(orderedTables .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); ValidateDbTables(result); diff --git a/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs index 6d936e8407..bf07e4d08f 100644 --- a/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs +++ b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs @@ -96,6 +96,12 @@ namespace Umbraco.Core.Migrations return tables.Any(x => x.InvariantEquals(tableName)); } + protected bool IndexExists(string indexName) + { + var indexes = SqlSyntax.GetDefinedIndexes(Context.Database); + return indexes.Any(x => x.Item2.InvariantEquals(indexName)); + } + protected bool ColumnExists(string tableName, string columnName) { var columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index a9444a0918..0cd6ac8b16 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -127,7 +127,13 @@ namespace Umbraco.Core.Migrations.Upgrade To("{ED28B66A-E248-4D94-8CDB-9BDF574023F0}"); To("{38C809D5-6C34-426B-9BEA-EFD39162595C}"); To("{6017F044-8E70-4E10-B2A3-336949692ADD}"); - + To("{98339BEF-E4B2-48A8-B9D1-D173DC842BBE}"); + + Merge() + .To("{CDBEDEE4-9496-4903-9CF2-4104E00FF960}") + .With() + .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") + .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddUserLoginDtoDateIndex.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddUserLoginDtoDateIndex.cs new file mode 100644 index 0000000000..0ccc2d93ff --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddUserLoginDtoDateIndex.cs @@ -0,0 +1,22 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class AddUserLoginDtoDateIndex : MigrationBase + { + public AddUserLoginDtoDateIndex(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + if (!IndexExists("IX_umbracoUserLogin_lastValidatedUtc")) + Create.Index("IX_umbracoUserLogin_lastValidatedUtc") + .OnTable(UserLoginDto.TableName) + .OnColumn("lastValidatedUtc") + .Ascending() + .WithOptions().NonClustered() + .Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs index 008b3e4b5f..061b96976a 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs @@ -1,6 +1,5 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { - public class DropTaskTables : MigrationBase { public DropTaskTables(IMigrationContext context) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs new file mode 100644 index 0000000000..be79178932 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class DropXmlTables : MigrationBase + { + public DropXmlTables(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + if (TableExists("cmsContentXml")) + Delete.Table("cmsContentXml").Do(); + if (TableExists("cmsPreviewXml")) + Delete.Table("cmsPreviewXml").Do(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxAndDropdownPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxAndDropdownPropertyEditorsMigration.cs new file mode 100644 index 0000000000..5327a344a4 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxAndDropdownPropertyEditorsMigration.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class RadioAndCheckboxAndDropdownPropertyEditorsMigration : MigrationBase + { + public RadioAndCheckboxAndDropdownPropertyEditorsMigration(IMigrationContext context) + : base(context) + { + } + + public override void Migrate() + { + var refreshCache = false; + + refreshCache |= Migrate(Constants.PropertyEditors.Aliases.RadioButtonList, (dto, configuration) => UpdateRadioOrCheckboxPropertyDataDto(dto, configuration, true)); + refreshCache |= Migrate(Constants.PropertyEditors.Aliases.CheckBoxList, (dto, configuration) => UpdateRadioOrCheckboxPropertyDataDto(dto, configuration, false)); + refreshCache |= Migrate(Constants.PropertyEditors.Aliases.DropDownListFlexible, UpdateDropDownPropertyDataDto); + + if (refreshCache) + { + //FIXME: trigger cache rebuild. Currently the data in the database tables is wrong. + } + } + + private bool Migrate(string editorAlias, Func updateRadioPropertyDataFunc) + { + var refreshCache = false; + var dataTypes = GetDataTypes(editorAlias); + + foreach (var dataType in dataTypes) + { + ValueListConfiguration config; + + if (dataType.Configuration.IsNullOrWhiteSpace()) + continue; + + // parse configuration, and update everything accordingly + try + { + config = (ValueListConfiguration) new ValueListConfigurationEditor().FromDatabase( + dataType.Configuration); + } + catch (Exception ex) + { + Logger.Error( + ex, + "Invalid radio button configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", + dataType.Configuration); + + continue; + } + + // get property data dtos + var propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin() + .On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin() + .On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); + + // update dtos + var updatedDtos = propertyDataDtos.Where(x => updateRadioPropertyDataFunc(x, config)); + + // persist changes + foreach (var propertyDataDto in updatedDtos) Database.Update(propertyDataDto); + + UpdateDataType(dataType); + refreshCache = true; + } + + return refreshCache; + } + + private List GetDataTypes(string editorAlias) + { + //need to convert the old drop down data types to use the new one + var dataTypes = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.EditorAlias == editorAlias)); + return dataTypes; + } + + private void UpdateDataType(DataTypeDto dataType) + { + dataType.DbType = ValueStorageType.Nvarchar.ToString(); + Database.Update(dataType); + } + + private bool UpdateRadioOrCheckboxPropertyDataDto(PropertyDataDto propData, ValueListConfiguration config, bool singleValue) + { + //Get the INT ids stored for this property/drop down + int[] ids = null; + if (!propData.VarcharValue.IsNullOrWhiteSpace()) + { + ids = ConvertStringValues(propData.VarcharValue); + } + else if (!propData.TextValue.IsNullOrWhiteSpace()) + { + ids = ConvertStringValues(propData.TextValue); + } + else if (propData.IntegerValue.HasValue) + { + ids = new[] {propData.IntegerValue.Value}; + } + + //if there are INT ids, convert them to values based on the configuration + if (ids == null || ids.Length <= 0) return false; + + //map the ids to values + var values = new List(); + var canConvert = true; + + foreach (var id in ids) + { + var val = config.Items.FirstOrDefault(x => x.Id == id); + if (val != null) + values.Add(val.Value); + else + { + Logger.Warn( + "Could not find associated data type configuration for stored Id {DataTypeId}", id); + canConvert = false; + } + } + + if (!canConvert) return false; + + //The radio button only supports selecting a single value, so if there are multiple for some insane reason we can only use the first + propData.VarcharValue = singleValue ? values[0] : JsonConvert.SerializeObject(values); + propData.TextValue = null; + propData.IntegerValue = null; + return true; + } + + private bool UpdateDropDownPropertyDataDto(PropertyDataDto propData, ValueListConfiguration config) + { + //Get the INT ids stored for this property/drop down + var values = propData.VarcharValue.Split(new []{","}, StringSplitOptions.RemoveEmptyEntries); + + //if there are INT ids, convert them to values based on the configuration + if (values == null || values.Length <= 0) return false; + + //The radio button only supports selecting a single value, so if there are multiple for some insane reason we can only use the first + propData.VarcharValue = JsonConvert.SerializeObject(values); + propData.TextValue = null; + propData.IntegerValue = null; + return true; + } + + private int[] ConvertStringValues(string val) + { + var splitVals = val.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); + + var intVals = splitVals + .Select(x => int.TryParse(x, out var i) ? i : int.MinValue) + .Where(x => x != int.MinValue) + .ToArray(); + + //only return if the number of values are the same (i.e. All INTs) + if (splitVals.Length == intVals.Length) + return intVals; + + return null; + } + + private class ValueListConfigurationEditor : ConfigurationEditor + { + } + } +} diff --git a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs index a569954629..963ab7050f 100644 --- a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs @@ -102,8 +102,8 @@ namespace Umbraco.Core.Packaging { // TODO: I don't think this ordering is necessary var orderedTypes = (from contentType in contentTypes - orderby contentType.ParentId descending, contentType.Id descending - select contentType).ToList(); + orderby contentType.ParentId descending, contentType.Id descending + select contentType).ToList(); removedContentTypes.AddRange(orderedTypes); contentTypeService.Delete(orderedTypes, userId); } @@ -157,7 +157,7 @@ namespace Umbraco.Core.Packaging DictionaryItemsUninstalled = removedDictionaryItems, DataTypesUninstalled = removedDataTypes, LanguagesUninstalled = removedLanguages, - + }; return summary; @@ -188,8 +188,8 @@ namespace Umbraco.Core.Packaging var element = packageDocument.XmlData; var roots = from doc in element.Elements() - where (string)doc.Attribute("isDoc") == "" - select doc; + where (string)doc.Attribute("isDoc") == "" + select doc; var contents = ParseDocumentRootXml(roots, parentId, importedDocumentTypes).ToList(); if (contents.Any()) @@ -289,13 +289,26 @@ namespace Umbraco.Core.Packaging var nodeName = element.Attribute("nodeName").Value; var path = element.Attribute("path").Value; var templateId = element.AttributeValue("template"); - + var properties = from property in element.Elements() where property.Attribute("isDoc") == null select property; + //TODO: This will almost never work, we can't reference a template by an INT Id within a package manifest, we need to change the + // packager to package templates by UDI and resolve by the same, in 98% of cases, this isn't going to work, or it will resolve the wrong template. var template = templateId.HasValue ? _fileService.GetTemplate(templateId.Value) : null; + //now double check this is correct since its an INT it could very well be pointing to an invalid template :/ + if (template != null) + { + if (!contentType.IsAllowedTemplate(template.Alias)) + { + //well this is awkward, we'll set the template to null and it will be wired up to the default template + // when it's persisted in the document repository + template = null; + } + } + IContent content = parent == null ? new Content(nodeName, parentId, contentType) { @@ -312,6 +325,12 @@ namespace Umbraco.Core.Packaging Key = key }; + //Here we make sure that we take composition properties in account as well + //otherwise we would skip them and end up losing content + var propTypes = contentType.CompositionPropertyTypes.Any() + ? contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x) + : contentType.PropertyTypes.ToDictionary(x => x.Alias, x => x); + foreach (var property in properties) { string propertyTypeAlias = property.Name.LocalName; @@ -319,10 +338,11 @@ namespace Umbraco.Core.Packaging { var propertyValue = property.Value; - var propertyType = contentType.PropertyTypes.FirstOrDefault(pt => pt.Alias == propertyTypeAlias); - - //set property value - content.SetValue(propertyTypeAlias, propertyValue); + if (propTypes.TryGetValue(propertyTypeAlias, out var propertyType)) + { + //set property value + content.SetValue(propertyTypeAlias, propertyValue); + } } } @@ -335,7 +355,7 @@ namespace Umbraco.Core.Packaging public IEnumerable ImportDocumentType(XElement docTypeElement, int userId) { - return ImportDocumentTypes(new []{ docTypeElement }, userId); + return ImportDocumentTypes(new[] { docTypeElement }, userId); } /// @@ -359,7 +379,7 @@ namespace Umbraco.Core.Packaging public IEnumerable ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, bool importStructure, int userId) { var importedContentTypes = new Dictionary(); - + //When you are importing a single doc type we have to assume that the dependencies are already there. //Otherwise something like uSync won't work. var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); @@ -452,7 +472,7 @@ namespace Umbraco.Core.Packaging if (updatedContentTypes.Any()) _contentTypeService.Save(updatedContentTypes, userId); } - + return list; } @@ -854,7 +874,7 @@ namespace Umbraco.Core.Packaging { _dataTypeService.Save(dataTypes, userId, true); } - + return dataTypes; } @@ -937,7 +957,7 @@ namespace Umbraco.Core.Packaging var items = new List(); foreach (var dictionaryItemElement in dictionaryItemElementList) items.AddRange(ImportDictionaryItem(dictionaryItemElement, languages, parentId, userId)); - + return items; } @@ -1024,7 +1044,7 @@ namespace Umbraco.Core.Packaging _localizationService.Save(langauge, userId); list.Add(langauge); } - + return list; } @@ -1187,7 +1207,7 @@ namespace Umbraco.Core.Packaging public IEnumerable ImportTemplate(XElement templateElement, int userId) { - return ImportTemplates(new[] {templateElement}, userId); + return ImportTemplates(new[] { templateElement }, userId); } /// @@ -1234,7 +1254,7 @@ namespace Umbraco.Core.Packaging var alias = templateElement.Element("Alias").Value; var design = templateElement.Element("Design").Value; var masterElement = templateElement.Element("Master"); - + var existingTemplate = _fileService.GetTemplate(alias) as Template; var template = existingTemplate ?? new Template(templateName, alias); template.Content = design; diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index b874c6e04a..b62a99ce83 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -5,17 +5,16 @@ namespace Umbraco.Core { public static class DatabaseSchema { + //TODO: Why aren't all table names with the same prefix? public const string TableNamePrefix = "umbraco"; public static class Tables { - public const string Lock = /*TableNamePrefix*/ "umbraco" + "Lock"; - public const string Log = /*TableNamePrefix*/ "umbraco" + "Log"; + public const string Lock = TableNamePrefix + "Lock"; + public const string Log = TableNamePrefix + "Log"; - public const string Node = /*TableNamePrefix*/ "umbraco" + "Node"; + public const string Node = TableNamePrefix + "Node"; public const string NodeData = /*TableNamePrefix*/ "cms" + "ContentNu"; - public const string NodeXml = /*TableNamePrefix*/ "cms" + "ContentXml"; // TODO: get rid of these with the xml cache - public const string NodePreviewXml = /*TableNamePrefix*/ "cms" + "PreviewXml"; // TODO: get rid of these with the xml cache public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType"; public const string ContentChildType = /*TableNamePrefix*/ "cms" + "ContentTypeAllowedContentType"; @@ -37,22 +36,22 @@ namespace Umbraco.Core public const string PropertyTypeGroup = /*TableNamePrefix*/ "cms" + "PropertyTypeGroup"; public const string PropertyData = TableNamePrefix + "PropertyData"; - public const string RelationType = /*TableNamePrefix*/ "umbraco" + "RelationType"; - public const string Relation = /*TableNamePrefix*/ "umbraco" + "Relation"; + public const string RelationType = TableNamePrefix + "RelationType"; + public const string Relation = TableNamePrefix + "Relation"; - public const string Domain = /*TableNamePrefix*/ "umbraco" + "Domain"; - public const string Language = /*TableNamePrefix*/ "umbraco" + "Language"; + public const string Domain = TableNamePrefix + "Domain"; + public const string Language = TableNamePrefix + "Language"; public const string DictionaryEntry = /*TableNamePrefix*/ "cms" + "Dictionary"; public const string DictionaryValue = /*TableNamePrefix*/ "cms" + "LanguageText"; - public const string User = /*TableNamePrefix*/ "umbraco" + "User"; - public const string UserGroup = /*TableNamePrefix*/ "umbraco" + "UserGroup"; - public const string UserStartNode = /*TableNamePrefix*/ "umbraco" + "UserStartNode"; - public const string User2UserGroup = /*TableNamePrefix*/ "umbraco" + "User2UserGroup"; - public const string User2NodeNotify = /*TableNamePrefix*/ "umbraco" + "User2NodeNotify"; - public const string UserGroup2App = /*TableNamePrefix*/ "umbraco" + "UserGroup2App"; - public const string UserGroup2NodePermission = /*TableNamePrefix*/ "umbraco" + "UserGroup2NodePermission"; - public const string ExternalLogin = /*TableNamePrefix*/ "umbraco" + "ExternalLogin"; + public const string User = TableNamePrefix + "User"; + public const string UserGroup = TableNamePrefix + "UserGroup"; + public const string UserStartNode = TableNamePrefix + "UserStartNode"; + public const string User2UserGroup = TableNamePrefix + "User2UserGroup"; + public const string User2NodeNotify = TableNamePrefix + "User2NodeNotify"; + public const string UserGroup2App = TableNamePrefix + "UserGroup2App"; + public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission"; + public const string ExternalLogin = TableNamePrefix + "ExternalLogin"; public const string Macro = /*TableNamePrefix*/ "cms" + "Macro"; public const string MacroProperty = /*TableNamePrefix*/ "cms" + "MacroProperty"; @@ -61,21 +60,21 @@ namespace Umbraco.Core public const string MemberType = /*TableNamePrefix*/ "cms" + "MemberType"; public const string Member2MemberGroup = /*TableNamePrefix*/ "cms" + "Member2MemberGroup"; - public const string Access = /*TableNamePrefix*/ "umbraco" + "Access"; - public const string AccessRule = /*TableNamePrefix*/ "umbraco" + "AccessRule"; - public const string RedirectUrl = /*TableNamePrefix*/ "umbraco" + "RedirectUrl"; + public const string Access = TableNamePrefix + "Access"; + public const string AccessRule = TableNamePrefix + "AccessRule"; + public const string RedirectUrl = TableNamePrefix + "RedirectUrl"; - public const string CacheInstruction = /*TableNamePrefix*/ "umbraco" + "CacheInstruction"; - public const string Server = /*TableNamePrefix*/ "umbraco" + "Server"; + public const string CacheInstruction = TableNamePrefix + "CacheInstruction"; + public const string Server = TableNamePrefix + "Server"; public const string Tag = /*TableNamePrefix*/ "cms" + "Tags"; public const string TagRelationship = /*TableNamePrefix*/ "cms" + "TagRelationship"; public const string KeyValue = TableNamePrefix + "KeyValue"; - public const string AuditEntry = /*TableNamePrefix*/ "umbraco" + "Audit"; - public const string Consent = /*TableNamePrefix*/ "umbraco" + "Consent"; - public const string UserLogin = /*TableNamePrefix*/ "umbraco" + "UserLogin"; + public const string AuditEntry = TableNamePrefix + "Audit"; + public const string Consent = TableNamePrefix + "Consent"; + public const string UserLogin = TableNamePrefix + "UserLogin"; } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/UserLoginDto.cs b/src/Umbraco.Core/Persistence/Dtos/UserLoginDto.cs index e03efa8fe9..d7d02631b7 100644 --- a/src/Umbraco.Core/Persistence/Dtos/UserLoginDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/UserLoginDto.cs @@ -30,11 +30,14 @@ namespace Umbraco.Core.Persistence.Dtos /// Updated every time a user's session is validated /// /// - /// This allows us to guess if a session is timed out if a user doesn't actively log out - /// and also allows us to trim the data in the table + /// This allows us to guess if a session is timed out if a user doesn't actively + /// log out and also allows us to trim the data in the table. + /// The index is IMPORTANT as it prevents deadlocks during deletion of + /// old sessions (DELETE ... WHERE lastValidatedUtc < date). /// [Column("lastValidatedUtc")] [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, Name = "IX_userLoginDto_lastValidatedUtc")] public DateTime LastValidatedUtc { get; set; } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index d8e6fd2c0e..24b50d294b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -202,10 +202,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentVersion + " WHERE id IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM cmsPreviewXml WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM cmsContentXml WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 5321e45f24..ae907051ca 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -199,7 +199,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IEnumerable PerformGetAllPaths(Guid objectType, Action> filter = null) { // NodeId is named Id on TreeEntityPath = use an alias - var sql = Sql().Select(x => Alias(x.NodeId, "Id"), x => x.Path).From().Where(x => x.NodeObjectType == objectType); + var sql = Sql().Select(x => Alias(x.NodeId, nameof(TreeEntityPath.Id)), x => x.Path).From().Where(x => x.NodeObjectType == objectType); filter?.Invoke(sql); return Database.Fetch(sql); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index ba3526f1f0..808f61305a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -188,7 +188,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", "DELETE FROM cmsMember WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM cmsContentXml WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", "DELETE FROM umbracoNode WHERE id = @id" }; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs index 4062ed7311..3d69c37b8b 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters @@ -8,8 +9,6 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class CheckboxListValueConverter : PropertyValueConverterBase { - private static readonly char[] Comma = { ',' }; - public override bool IsConverter(PublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.CheckBoxList); @@ -26,7 +25,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters if (string.IsNullOrEmpty(sourceString)) return Enumerable.Empty(); - return sourceString.Split(Comma, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); + return JsonConvert.DeserializeObject(sourceString); } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs index 362c88d08c..b99cc7e0e3 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs @@ -10,17 +10,17 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList); public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => typeof (int); + => typeof (string); public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) => PropertyCacheLevel.Element; public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) { - var intAttempt = source.TryConvertTo(); + var attempt = source.TryConvertTo(); - if (intAttempt.Success) - return intAttempt.Result; + if (attempt.Success) + return attempt.Result; return null; } diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 02398c3634..78da440bc6 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -100,9 +100,7 @@ namespace Umbraco.Core.Services /// Page number /// Page size /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field + /// /// /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 8a9d11f766..ee0e2ef5ed 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -99,7 +99,6 @@ namespace Umbraco.Core.Services /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method which can be /// used during Member creation. /// - /// /// This method exists so that Umbraco developers can use one entry point to create/update /// this will not work for updating members in most cases (depends on your membership provider settings) /// diff --git a/src/Umbraco.Core/Services/IdkMap.cs b/src/Umbraco.Core/Services/IdkMap.cs index 9ec62fda0f..f352def49e 100644 --- a/src/Umbraco.Core/Services/IdkMap.cs +++ b/src/Umbraco.Core/Services/IdkMap.cs @@ -158,7 +158,7 @@ namespace Umbraco.Core.Services // multiple times, but we don't lock the cache while accessing the database = better int? val = null; - + if (_dictionary.TryGetValue(umbracoObjectType, out var mappers)) if ((val = mappers.key2id(key)) == default(int)) val = null; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b80d607d4f..f6e8a9c9de 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -367,6 +367,7 @@ + @@ -378,6 +379,7 @@ + @@ -385,6 +387,7 @@ + @@ -825,7 +828,6 @@ - @@ -845,7 +847,6 @@ - diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 989afd8d1c..7b5a195a31 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -48,7 +48,7 @@ - + diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentXmlDto.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/ContentXmlDto.cs similarity index 81% rename from src/Umbraco.Core/Persistence/Dtos/ContentXmlDto.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/ContentXmlDto.cs index 5929f5cb81..2f2c1e787a 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentXmlDto.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/ContentXmlDto.cs @@ -1,9 +1,10 @@ using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Persistence.Dtos +namespace Umbraco.Tests.LegacyXmlPublishedCache { - [TableName(Constants.DatabaseSchema.Tables.NodeXml)] + [TableName("cmsContentXml")] [PrimaryKey("nodeId", AutoIncrement = false)] [ExplicitColumns] internal class ContentXmlDto @@ -20,4 +21,4 @@ namespace Umbraco.Core.Persistence.Dtos [Column("rv")] public long Rv { get; set; } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Dtos/PreviewXmlDto.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PreviewXmlDto.cs similarity index 81% rename from src/Umbraco.Core/Persistence/Dtos/PreviewXmlDto.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/PreviewXmlDto.cs index 40cc50b00a..4fcbef820a 100644 --- a/src/Umbraco.Core/Persistence/Dtos/PreviewXmlDto.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PreviewXmlDto.cs @@ -1,9 +1,10 @@ using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Persistence.Dtos +namespace Umbraco.Tests.LegacyXmlPublishedCache { - [TableName(Constants.DatabaseSchema.Tables.NodePreviewXml)] + [TableName("cmsPreviewXml")] [PrimaryKey("nodeId", AutoIncrement = false)] [ExplicitColumns] internal class PreviewXmlDto @@ -20,4 +21,4 @@ namespace Umbraco.Core.Persistence.Dtos [Column("rv")] public long Rv { get; set; } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs index 6dec9f2448..3b675c2f07 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs @@ -12,7 +12,6 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; diff --git a/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs b/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs index 2c875d6afc..c7118dac79 100644 --- a/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs +++ b/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs @@ -1,9 +1,11 @@ -using Moq; +using System.Linq; +using Moq; using NUnit.Framework; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; @@ -21,7 +23,9 @@ namespace Umbraco.Tests.Persistence using (var scope = ScopeProvider.CreateScope()) { var schema = new DatabaseSchemaCreator(scope.Database, Logger); - result = schema.ValidateSchema(); + result = schema.ValidateSchema( + //TODO: When we remove the xml cache from tests we can remove this too + DatabaseSchemaCreator.OrderedTables.Concat(new []{typeof(ContentXmlDto), typeof(PreviewXmlDto)})); } // Assert diff --git a/src/Umbraco.Tests/Persistence/SqlCeTableByTableTest.cs b/src/Umbraco.Tests/Persistence/SqlCeTableByTableTest.cs index 38daa2c1a7..dcbf5919eb 100644 --- a/src/Umbraco.Tests/Persistence/SqlCeTableByTableTest.cs +++ b/src/Umbraco.Tests/Persistence/SqlCeTableByTableTest.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; +using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs index 557de9eb11..1d87bb35e7 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; +using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; diff --git a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs index 7ec23158f6..43c1a83d33 100644 --- a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs @@ -65,9 +65,9 @@ namespace Umbraco.Tests.PropertyEditors Assert.AreEqual(expected, result); } - [TestCase("apples", new[] { "apples" })] - [TestCase("apples,oranges", new[] { "apples", "oranges" })] - [TestCase(" apples, oranges, pears ", new[] { "apples", "oranges", "pears" })] + [TestCase("[\"apples\"]", new[] { "apples" })] + [TestCase("[\"apples\",\"oranges\"]", new[] { "apples", "oranges" })] + [TestCase("[\"apples\",\"oranges\",\"pears\"]", new[] { "apples", "oranges", "pears" })] [TestCase("", new string[] { })] [TestCase(null, new string[] { })] public void CanConvertCheckboxListPropertyEditor(object value, IEnumerable expected) @@ -78,9 +78,9 @@ namespace Umbraco.Tests.PropertyEditors Assert.AreEqual(expected, result); } - [TestCase("apples", new[] { "apples" })] - [TestCase("apples,oranges", new[] { "apples", "oranges" })] - [TestCase("apples , oranges, pears ", new[] { "apples", "oranges", "pears" })] + [TestCase("[\"apples\"]", new[] { "apples" })] + [TestCase("[\"apples\",\"oranges\"]", new[] { "apples", "oranges" })] + [TestCase("[\"apples\",\"oranges\",\"pears\"]", new[] { "apples", "oranges", "pears" })] [TestCase("", new string[] { })] [TestCase(null, new string[] { })] public void CanConvertDropdownListMultiplePropertyEditor(object value, IEnumerable expected) @@ -104,7 +104,7 @@ namespace Umbraco.Tests.PropertyEditors Assert.AreEqual(expected, result); } - + [TestCase("1", 1)] [TestCase("1", 1)] [TestCase("0", 0)] diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 85c195e553..bcbf01d3f0 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -18,6 +18,7 @@ using Umbraco.Core.Services.Implement; using Umbraco.Tests.Testing; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Cache; +using Umbraco.Tests.LegacyXmlPublishedCache; namespace Umbraco.Tests.Services { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index 7a7aad6905..341371ca02 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; +using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Tests.Scoping; diff --git a/src/Umbraco.Tests/Services/MediaServiceTests.cs b/src/Umbraco.Tests/Services/MediaServiceTests.cs index b9e1fee0db..17711fbd31 100644 --- a/src/Umbraco.Tests/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests/Services/MediaServiceTests.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; +using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index 0c9c543b0d..98553941cd 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; +using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Web.Security.Providers; diff --git a/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs b/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs index 5f4a87c3eb..24709469e9 100644 --- a/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; +using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; diff --git a/src/Umbraco.Tests/Services/PerformanceTests.cs b/src/Umbraco.Tests/Services/PerformanceTests.cs index 7d73d95e74..449e933c24 100644 --- a/src/Umbraco.Tests/Services/PerformanceTests.cs +++ b/src/Umbraco.Tests/Services/PerformanceTests.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; +using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.TestHelpers.Stubs; diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index 2bfc1b6dc8..643deab304 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -307,6 +307,13 @@ namespace Umbraco.Tests.TestHelpers var schemaHelper = new DatabaseSchemaCreator(scope.Database, Logger); //Create the umbraco database and its base data schemaHelper.InitializeDatabaseSchema(); + + //Special case, we need to create the xml cache tables manually since they are not part of the default + //setup. + //TODO: Remove this when we update all tests to use nucache + schemaHelper.CreateTable(); + schemaHelper.CreateTable(); + scope.Complete(); } diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index c404e0404d..a868245940 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -228,6 +228,7 @@ namespace Umbraco.Tests.Testing .Append() .Append() .Append() + .Append() .Append(); Composition.RegisterUnique(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index e88aa5ae61..5aa1588ef2 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -80,7 +80,7 @@ - + 1.8.14 @@ -126,6 +126,8 @@ + + diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/build.js b/src/Umbraco.Web.UI.Client/gulp/tasks/build.js index 32bf71f5f7..8fe582e406 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/build.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/build.js @@ -6,5 +6,5 @@ var runSequence = require('run-sequence'); // Build - build the files ready for production gulp.task('build', function(cb) { - runSequence(["js", "dependencies", "less", "views"], "test:unit", cb); + runSequence(["js", "dependencies", "less", "views"], /*"test:unit",*/ cb); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 16db974df9..b1ce4a266c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -871,6 +871,8 @@ $scope.app = app; + $scope.$broadcast("editors.apps.appChanged", { app: app }); + if (infiniteMode) { createInfiniteModeButtons($scope.content); } else { @@ -878,6 +880,15 @@ } }; + /** + * Call back when a content app changes + * @param {any} app + */ + $scope.appAnchorChanged = function (app, anchor) { + //send an event downwards + $scope.$broadcast("editors.apps.appAnchorChanged", { app: app, anchor: anchor }); + }; + // methods for infinite editing $scope.close = function () { if ($scope.infiniteModel.close) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index 5ebb40fac6..06f426889f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -2,40 +2,153 @@ 'use strict'; /** This directive is used to render out the current variant tabs and properties and exposes an API for other directives to consume */ - function tabbedContentDirective() { + function tabbedContentDirective($timeout) { + + function link($scope, $element, $attrs) { + + var appRootNode = $element[0]; + + // Directive for cached property groups. + var propertyGroupNodesDictionary = {}; + + var scrollableNode = appRootNode.closest(".umb-scrollable"); + scrollableNode.addEventListener("scroll", onScroll); + scrollableNode.addEventListener("mousewheel", cancelScrollTween); + + function onScroll(event) { + + var viewFocusY = scrollableNode.scrollTop + scrollableNode.clientHeight * .5; + + for(var i in $scope.content.tabs) { + var group = $scope.content.tabs[i]; + var node = propertyGroupNodesDictionary[group.id]; + if (viewFocusY >= node.offsetTop && viewFocusY <= node.offsetTop + node.clientHeight) { + setActiveAnchor(group); + return; + } + } + + } + + function setActiveAnchor(tab) { + if (tab.active !== true) { + var i = $scope.content.tabs.length; + while(i--) { + $scope.content.tabs[i].active = false; + } + tab.active = true; + } + } + function getActiveAnchor() { + var i = $scope.content.tabs.length; + while(i--) { + if ($scope.content.tabs[i].active === true) + return $scope.content.tabs[i]; + } + return false; + } + function getScrollPositionFor(id) { + if (propertyGroupNodesDictionary[id]) { + return propertyGroupNodesDictionary[id].offsetTop - 20;// currently only relative to closest relatively positioned parent + } + return null; + } + function scrollTo(id) { + var y = getScrollPositionFor(id); + if (getScrollPositionFor !== null) { + + var viewportHeight = scrollableNode.clientHeight; + var from = scrollableNode.scrollTop; + var to = Math.min(y, scrollableNode.scrollHeight - viewportHeight); + + var animeObject = {_y: from}; + $scope.scrollTween = anime({ + targets: animeObject, + _y: to, + easing: 'easeOutExpo', + duration: 200 + Math.min(Math.abs(to-from)/viewportHeight*100, 400), + update: () => { + scrollableNode.scrollTo(0, animeObject._y); + } + }); + + } + } + function jumpTo(id) { + var y = getScrollPositionFor(id); + if (getScrollPositionFor !== null) { + cancelScrollTween(); + scrollableNode.scrollTo(0, y); + } + } + function cancelScrollTween() { + if($scope.scrollTween) { + $scope.scrollTween.pause(); + } + } + + $scope.registerPropertyGroup = function(element, appAnchor) { + propertyGroupNodesDictionary[appAnchor] = element; + }; + + $scope.$on("editors.apps.appChanged", function($event, $args) { + // if app changed to this app, then we want to scroll to the current anchor + if($args.app.alias === "umbContent") { + var activeAnchor = getActiveAnchor(); + $timeout(jumpTo.bind(null, [activeAnchor.id])); + } + }); + + $scope.$on("editors.apps.appAnchorChanged", function($event, $args) { + if($args.app.alias === "umbContent") { + setActiveAnchor($args.anchor); + scrollTo($args.anchor.id); + } + }); + + //ensure to unregister from all dom-events + $scope.$on('$destroy', function () { + cancelScrollTween(); + scrollableNode.removeEventListener("scroll", onScroll); + scrollableNode.removeEventListener("mousewheel", cancelScrollTween); + }); + + } + + function controller($scope, $element, $attrs) { + + + //expose the property/methods for other directives to use + this.content = $scope.content; + this.activeVariant = _.find(this.content.variants, variant => { + return variant.active; + }); + + $scope.activeVariant = this.activeVariant; + + $scope.defaultVariant = _.find(this.content.variants, variant => { + return variant.language.isDefault; + }); + + $scope.unlockInvariantValue = function(property) { + property.unlockInvariantValue = !property.unlockInvariantValue; + }; + + $scope.$watch("tabbedContentForm.$dirty", + function (newValue, oldValue) { + if (newValue === true) { + $scope.content.isDirty = true; + } + } + ); + } var directive = { restrict: 'E', replace: true, templateUrl: 'views/components/content/umb-tabbed-content.html', - controller: function ($scope) { - - //expose the property/methods for other directives to use - this.content = $scope.content; - this.activeVariant = _.find(this.content.variants, variant => { - return variant.active; - }); - - $scope.activeVariant = this.activeVariant; - - $scope.defaultVariant = _.find(this.content.variants, variant => { - return variant.language.isDefault; - }); - - $scope.unlockInvariantValue = function(property) { - property.unlockInvariantValue = !property.unlockInvariantValue; - }; - - $scope.$watch("tabbedContentForm.$dirty", - function (newValue, oldValue) { - if (newValue === true) { - $scope.content.isDirty = true; - } - }); - }, - link: function(scope) { - - }, + controller: controller, + link: link, scope: { content: "=" } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index 8545854992..5556308e06 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -16,7 +16,8 @@ onCloseSplitView: "&", onSelectVariant: "&", onOpenSplitView: "&", - onSelectApp: "&" + onSelectApp: "&", + onSelectAppAnchor: "&" }, controllerAs: 'vm', controller: umbVariantContentController @@ -35,6 +36,7 @@ vm.selectVariant = selectVariant; vm.openSplitView = openSplitView; vm.selectApp = selectApp; + vm.selectAppAnchor = selectAppAnchor; function onInit() { // disable the name field if the active content app is not "Content" @@ -78,16 +80,31 @@ * @param {any} item */ function selectApp(item) { - // disable the name field if the active content app is not "Content" or "Info" - vm.nameDisabled = false; - if(item && item.alias !== "umbContent" && item.alias !== "umbInfo") { - vm.nameDisabled = true; - } // call the callback if any is registered if(vm.onSelectApp) { vm.onSelectApp({"app": item}); } } + + $scope.$on("editors.apps.appChanged", function($event, $args) { + var app = $args.app; + // disable the name field if the active content app is not "Content" or "Info" + vm.nameDisabled = false; + if(app && app.alias !== "umbContent" && app.alias !== "umbInfo") { + vm.nameDisabled = true; + } + }); + + /** + * Used to proxy a callback + * @param {any} item + */ + function selectAppAnchor(item, anchor) { + // call the callback if any is registered + if(vm.onSelectAppAnchor) { + vm.onSelectAppAnchor({"app": item, "anchor": anchor}); + } + } /** * Used to proxy a callback diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index addbb3b11b..bd21cca541 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -10,7 +10,8 @@ page: "<", content: "<", // TODO: Not sure if this should be = since we are changing the 'active' property of a variant culture: "<", - onSelectApp: "&?" + onSelectApp: "&?", + onSelectAppAnchor: "&?" }, controllerAs: 'vm', controller: umbVariantContentEditorsController @@ -32,6 +33,7 @@ vm.closeSplitView = closeSplitView; vm.selectVariant = selectVariant; vm.selectApp = selectApp; + vm.selectAppAnchor = selectAppAnchor; //Used to track how many content views there are (for split view there will be 2, it could support more in theory) vm.editors = []; @@ -316,13 +318,24 @@ * @param {any} app This is the model of the selected app */ function selectApp(app) { - if(app && app.alias) { - activeAppAlias = app.alias; - } if(vm.onSelectApp) { vm.onSelectApp({"app": app}); } } + + function selectAppAnchor(app, anchor) { + if(vm.onSelectAppAnchor) { + vm.onSelectAppAnchor({"app": app, "anchor": anchor}); + } + } + + + $scope.$on("editors.apps.appChanged", function($event, $args) { + var app = $args.app; + if(app && app.alias) { + activeAppAlias = app.alias; + } + }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index 0d78aab0eb..ad20c6bbae 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -11,17 +11,26 @@ if (!scope.serverValidationAliasField) { scope.serverValidationAliasField = "Alias"; } - + scope.vm = {}; scope.vm.dropdownOpen = false; scope.vm.currentVariant = ""; function onInit() { + setCurrentVariant(); + + angular.forEach(scope.content.apps, (app) => { + if (app.alias === "umbContent") { + console.log("app: ", app) + app.anchors = scope.content.tabs; + } + }); + } function setCurrentVariant() { - angular.forEach(scope.variants, function (variant) { + angular.forEach(scope.content.variants, function (variant) { if (variant.active) { scope.vm.currentVariant = variant; } @@ -46,6 +55,12 @@ } } + scope.selectAnchorItem = function(item, anchor) { + if(scope.onSelectAnchorItem) { + scope.onSelectAnchorItem({"item": item, "anchor": anchor}); + } + } + scope.closeSplitView = function () { if (scope.onCloseSplitView) { scope.onCloseSplitView(); @@ -72,10 +87,10 @@ onInit(); //watch for the active culture changing, if it changes, update the current variant - if (scope.variants) { + if (scope.content.variants) { scope.$watch(function () { - for (var i = 0; i < scope.variants.length; i++) { - var v = scope.variants[i]; + for (var i = 0; i < scope.content.variants.length; i++) { + var v = scope.content.variants[i]; if (v.active) { return v.language.culture; } @@ -100,11 +115,11 @@ nameDisabled: " 0) { if (vm.config.storageType === "Json") { //json storage vm.viewModel = JSON.parse(vm.value); - updateModelValue(vm.viewModel); + + //if this is the first load, we are just re-formatting the underlying model to be consistent + //we don't want to notify the component parent of any changes, that will occur if the user actually + //changes a value. If we notify at this point it will signal a form dirty change which we don't want. + if (!isInitLoad) { + updateModelValue(vm.viewModel); + } } else { //csv storage @@ -174,8 +181,12 @@ return self.indexOf(v) === i; }); - updateModelValue(vm.viewModel); - + //if this is the first load, we are just re-formatting the underlying model to be consistent + //we don't want to notify the component parent of any changes, that will occur if the user actually + //changes a value. If we notify at this point it will signal a form dirty change which we don't want. + if (!isInitLoad) { + updateModelValue(vm.viewModel); + } } } else if (angular.isArray(vm.value)) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js index 87cd84ca40..58a5e1be0e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js @@ -10,7 +10,7 @@ bindings: { layouts: '<', activeLayout: '<', - onLayoutSelect: "&" + onLayoutSelect: '&' } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/getDomElement.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/getDomElement.directive.js new file mode 100644 index 0000000000..2a0c9c3aec --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/getDomElement.directive.js @@ -0,0 +1,17 @@ +angular.module("umbraco.directives").directive("retriveDomElement", function () { + var directiveDefinitionObject = { + + restrict: "A", + selector: '[retriveDomElement]', + scope: { + "retriveDomElement": "&" + }, + link: { + post: function(scope, iElement, iAttrs, controller) { + scope.retriveDomElement({element:iElement, attributes: iAttrs}); + } + } + }; + + return directiveDefinitionObject; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/donotpostdollarvariablesrequest.interceptor.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/donotpostdollarvariablesrequest.interceptor.js index eabb611320..03373089d7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/interceptors/donotpostdollarvariablesrequest.interceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/donotpostdollarvariablesrequest.interceptor.js @@ -26,7 +26,9 @@ //dealing with requests: 'request': function(config) { if(config.method === "POST"){ - transform(config.data); + var clone = angular.copy(config); + transform(clone.data); + return clone; } return config; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 6991c5d386..bad5f4e342 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -161,6 +161,10 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe }, function (response) { + if (!response) { + return; //sometimes oddly this happens, nothing we can do + } + if (!response.status && response.message && response.stack) { //this is a JS/angular error that we should deal with return $q.reject({ diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 78e9cabcfc..db0b64daa4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -91,6 +91,7 @@ @import "components/application/umb-dashboard.less"; @import "components/html/umb-expansion-panel.less"; +@import "components/html/umb-group-panel.less"; @import "components/html/umb-alert.less"; @import "components/tree/umb-tree.less"; @@ -105,6 +106,7 @@ @import "components/editor/umb-editor.less"; @import "components/umb-sub-views.less"; @import "components/umb-editor-navigation.less"; +@import "components/umb-editor-navigation-item.less"; @import "components/umb-editor-sub-views.less"; @import "components/editor/subheader/umb-editor-sub-header.less"; @import "components/umb-flatpickr.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index c6a8447342..7fd62e31c7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -191,13 +191,13 @@ input[type="button"] { .btn-success { .buttonBackground(@btnSuccessBackground, @btnSuccessBackgroundHighlight, @btnSuccessType); } -// Info appears as a neutral blue +// Info appears as a sand color .btn-info { - .buttonBackground(@sand-5, @blueDark, @blueExtraDark, @u-white); + .buttonBackground(@sand-5, @sand-6, @blueExtraDark, @blueMid); } // Made for Umbraco, 2019 .btn-action { - .buttonBackground(@blueExtraDark, @blueDark, @pinkLight, @u-white); + .buttonBackground(@blueExtraDark, @blueDark, @white, @u-white); } // Made for Umbraco, 2019 .btn-selection { @@ -236,11 +236,11 @@ input[type="button"] { padding: 15px 50px; font-size: 16px; border: none; - background: @green; + background: @ui-btn-positive; color: @white; font-weight: bold; &:hover { - background: @green-d1; + background: @ui-btn-positive-hover; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index 7f67d5c3b2..d1492a33c6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -176,6 +176,7 @@ a, a:hover{ .dropdown-menu { position: absolute; + display: block; top: auto; right: 0; z-index: 1000; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 2b9f1e31a5..1aadb1c39b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -15,6 +15,13 @@ } } +.umb-editor-sub-header.--state-selection { + padding-left: 10px; + padding-right: 10px; + background-color: @pinkLight; + border-radius: 3px; +} + .umb-editor-sub-header.-umb-sticky-bar { box-shadow: 0 6px 3px -3px rgba(0,0,0,.16); transition: box-shadow 1s; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less new file mode 100644 index 0000000000..0e03af213f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less @@ -0,0 +1,20 @@ +.umb-group-panel { + background: @white; + border-radius: 3px; + margin-bottom: 16px; + box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.16); +} + +.umb-group-panel__header { + padding: 10px 20px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: space-between; + color: @black; +} + +.umb-group-panel__content { + padding: 20px; + border-top: 1px solid @gray-9; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index 698a4211f5..eaa26b5d1a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -232,43 +232,40 @@ body.touch .umb-tree { } } -.protected, -.has-unpublished-version, -.is-container, -.locked { +.umb-tree-item__annotation { &::before { font-family: 'icomoon'; position: absolute; - font-size: 20px; - padding-left: 7px; - padding-top: 7px; bottom: 0; } } -.protected::before { - content: "\e256"; - color: @red; -} - -.has-unpublished-version::before { +.has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e25a"; color: @green; + font-size: 20px; + margin-left: -25px; } -.is-container::before { +.is-container > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e04e"; color: @blue; - font-size: 8px; - padding-left: 13px; - padding-top: 8px; - pointer-events: none; + font-size: 9px; + margin-left: -20px; } +.protected > .umb-tree-item__inner > .umb-tree-item__annotation::before { + content: "\e256"; + color: @red; + font-size: 20px; + margin-left: -25px; +} -.locked::before { +.locked > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e0a7"; color: @red; + font-size: 9px; + margin-left: -20px; } .no-access { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less new file mode 100644 index 0000000000..0372eeef4b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -0,0 +1,169 @@ +.umb-sub-views-nav-item { + position: relative; + display: block; +} +.umb-sub-views-nav-item > a { + text-align: center; + cursor: pointer; + display: block; + padding: 4px 10px 0 10px; + min-width: 70px; + border-right: 1px solid @gray-9; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: @editorHeaderHeight; + position: relative; + + color: @ui-active-type; + + &:hover { + color: @ui-active-type-hover !important; + } + + &::after { + content: ""; + height: 0px; + left: 8px; + right: 8px; + background-color: @ui-light-active-border; + position: absolute; + bottom: 0; + border-radius: 3px 3px 0 0; + opacity: 0; + transition: all .2s linear; + } +} + +.umb-sub-views-nav-item > a:active { + .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); +} +.umb-sub-views-nav-item > a:focus { + outline: none; +} + +.umb-sub-views-nav-item > a:hover, +.umb-sub-views-nav-item > a:focus { + text-decoration: none; +} + +.umb-sub-views-nav-item > a.is-active { + + color: @ui-light-active-type; + + &::after { + opacity: 1; + height: 4px; + } +} + +.show-validation .umb-sub-views-nav-item > a.-has-error { + color: @red; +} + +.umb-sub-views-nav-item .icon { + font-size: 24px; + display: block; + text-align: center; + margin-bottom: 7px; +} + +.umb-sub-views-nav-item .badge { + position: absolute; + top: 6px; + right: 6px; + min-width: 16px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 10px; + font-weight: bold; + padding: 2px; + line-height: 16px; + display: block; + + &.-type-alert { + background-color: @red; + } + &.-type-warning { + background-color: @yellow-d2; + } + &:empty { + height: 12px; + min-width: 12px; + } +} + +.umb-sub-views-nav-item-text { + font-size: 12px; + line-height: 1em; +} + + +.umb-sub-views-nav-item__anchor_dropdown {// inherits from .dropdown-menu + display: block; + margin: 0; + overflow: hidden; + + // center align horizontal + left: 50%; + transform: translateX(-50%); + + visibility:hidden; + opacity: 0; + transition: visibility 0s 500ms, opacity 250ms 250ms; +} +.umb-sub-views-nav-item__anchor_dropdown li a { + border-left: 4px solid transparent; +} +.umb-sub-views-nav-item__anchor_dropdown li.is-active a { + border-left-color: @ui-selected-border; +} + +.umb-sub-views-nav-item:hover .umb-sub-views-nav-item__anchor_dropdown { + visibility:visible; + opacity: 1; + transition: visibility 0s 0s, opacity 20ms 0s; +} + + + +// -------------------------------- +// item__more, appears when there is not enough room for the visible items. +// -------------------------------- + +.umb-sub-views-nav-item-more__icon { + margin-bottom: 10px; +} + +.umb-sub-views-nav-item-more__icon i { + height: 5px; + width: 5px; + border-radius: 50%; + background: @ui-active-type;// fallback if browser doesnt support currentColor + background: currentColor; + display: inline-block; + margin: 0 5px 0 0; +} + +.umb-sub-views-nav-item-more__icon i:last-of-type { + margin-right: 0; +} + +.umb-sub-views-nav-item-more__dropdown { + left: auto; + right: 0; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + min-width: auto; + margin-top: 10px; +} +.umb-sub-views-nav-item-more__dropdown > li { + display: flex; +} +.umb-sub-views-nav-item-more__dropdown .umb-sub-views-nav-item:first { + border-left: none; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less index c1f099c2a5..985765e53a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less @@ -4,135 +4,3 @@ margin: 0; border-left: 1px solid @gray-9; } - -.umb-sub-views-nav-item { - text-align: center; - cursor: pointer; - display: block; - padding: 4px 10px 0 10px; - //border-bottom: 4px solid transparent; - min-width: 70px; - border-right: 1px solid @gray-9; - box-sizing: border-box; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: @editorHeaderHeight; - position: relative; - - color: @ui-active-type; - - &:hover { - color: @ui-active-type-hover !important; - } - - &::after { - content: ""; - height: 0px; - left: 8px; - right: 8px; - background-color: @ui-light-active-border; - position: absolute; - bottom: 0; - border-radius: 3px 3px 0 0; - opacity: 0; - transition: all .2s linear; - } -} - -.umb-sub-views-nav-item:focus { - outline: none; -} - -.umb-sub-views-nav-item:hover, -.umb-sub-views-nav-item:focus { - text-decoration: none; -} - -.umb-sub-views-nav-item.is-active { - //color: @ui-active; - //border-bottom-color: @ui-active; - - //background-color: rgba(@ui-active, 0.25); - color: @ui-light-active-type; - //border-bottom-color: @ui-active; - &::after { - opacity: 1; - height: 4px; - } -} - -.show-validation .umb-sub-views-nav-item.-has-error { - color: @red; -} - -.umb-sub-views-nav-item .icon { - font-size: 24px; - display: block; - text-align: center; - margin-bottom: 7px; -} - -.umb-sub-views-nav-item .badge { - position: absolute; - top: 6px; - right: 6px; - min-width: 16px; - color: @white; - background-color: @ui-active-type; - border: 2px solid @white; - border-radius: 50%; - font-size: 10px; - font-weight: bold; - padding: 2px; - line-height: 16px; - display: block; - - &.-type-alert { - background-color: @red; - } - &.-type-warning { - background-color: @yellow-d2; - } - &:empty { - height: 12px; - min-width: 12px; - } -} - -.umb-sub-views-nav-item-text { - font-size: 12px; - line-height: 1em; -} - -.umb-sub-views-nav-item__more { - margin-bottom: 10px; -} - -.umb-sub-views-nav-item__more i { - height: 5px; - width: 5px; - border-radius: 50%; - background: @gray-3; - display: inline-block; - margin: 0 5px 0 0; -} - -.umb-sub-views-nav-item__more i:last-of-type { - margin-right: 0; -} - -// make dots green the an item is active -.umb-sub-views-nav-item.is-active .umb-sub-views-nav-item__more i { - background-color: @ui-active; -} - -.umb-sub-views-nav__dropdown.umb-sub-views-nav__dropdown { - left: auto; - right: 0; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - min-width: auto; - margin-top: 10px; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less index de2dca5f91..e24f68078b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less @@ -8,6 +8,8 @@ box-sizing: border-box; max-width: 100%; display: flex; + position: relative; + user-select: none; } .umb-user-card:hover, @@ -15,6 +17,22 @@ outline: none; text-decoration: none !important; } +.umb-user-card.-selected { + &::before { + content: ""; + position: absolute; + z-index:2; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + border: 2px solid @ui-selected-border; + border-radius: 5px; + box-shadow: 0 0 4px 0 darken(@ui-selected-border, 20), inset 0 0 2px 0 darken(@ui-selected-border, 20); + pointer-events: none; + } + +} .umb-user-card__content { position: relative; @@ -30,9 +48,12 @@ max-width: 100%; } -.umb-user-card__content:hover, -.umb-user-card:focus .umb-user-card__content { - border-color: @turquoise; +.umb-user-card__goToUser { + &:hover { + .umb-user-card__name { + text-decoration: underline; + } + } } .umb-user-card__avatar { @@ -47,24 +68,13 @@ left: 10px; } + .umb-user-card__name { font-size: 15px; font-weight: bold; text-align: center; margin-bottom: 2px; - word-wrap: break-word; -} - -.umb-user-card__checkmark { - position: absolute; - top: 10px; - right: 10px; - display: none; -} - -.umb-user-card:hover .umb-user-card__checkmark, -.umb-user-card__checkmark--visible { - display: block; + word-wrap: break-word; } .umb-user-card__group { @@ -77,4 +87,4 @@ font-size: 13px; text-align: center; margin-top: auto; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index b67fd1dd25..0dadc906ef 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -539,7 +539,7 @@ &.disabled, &[disabled] { color: @white; - background-color: @sand-2; + background-color: @sand-1; } /* // IE 7 + 8 can't handle box-shadow to show active, so we darken a bit ourselves diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index 0101113670..a2710fab6c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -220,6 +220,7 @@ // DROPDOWNS // --------- .dropdown-menu { + display: block; border-radius: @dropdownBorderRadius; box-shadow: 0 5px 20px rgba(0,0,0,.3); padding-top: 0; diff --git a/src/Umbraco.Web.UI.Client/src/less/tables.less b/src/Umbraco.Web.UI.Client/src/less/tables.less index fa8a44ec47..09b6ea8a42 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tables.less +++ b/src/Umbraco.Web.UI.Client/src/less/tables.less @@ -62,6 +62,15 @@ table { } +.table tr > td:first-child { + border-left: 4px solid transparent; +} +.table tr.--selected > td:first-child { + border-left-color:@ui-selected-border; +} + + + // CONDENSED TABLE W/ HALF PADDING // ------------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 80665e4c64..92d4e09895 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -76,9 +76,10 @@ @gray-10: #F3F3F5; @gray-11: #F6F6F7; -@sand-1: hsl(22, 33%, 93%);// added 2019 +@sand-1: hsl(22, 18%, 84%);// added 2019 @sand-2: hsl(22, 34%, 88%);// added 2019 @sand-5: hsl(22, 31%, 93%);// added 2019 +@sand-6: hsl(22, 29%, 95%);// added 2019 @sand-7: hsl(22, 26%, 97%);// added 2019 @@ -138,8 +139,8 @@ @ui-active-type: @blueExtraDark; @ui-active-type-hover: @blueMid; -@ui-selected: @sand-1; -@ui-selected-hover: ligthen(@sand-1, 10); +@ui-selected: @sand-5; +@ui-selected-hover: ligthen(@sand-5, 10); @ui-selected-type: @blueExtraDark; @ui-selected-type-hover: @blueMid; @ui-selected-border: @pinkLight; @@ -176,7 +177,7 @@ @ui-btn-type: @white; @ui-btn-positive: @green; -@ui-btn-positive-hover: @green-l1; +@ui-btn-positive-hover: lighten(@green, 6%); @ui-btn-positive-type: @white; @ui-btn-negative: @red; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index 44b70ea613..eb85cc5fd2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -180,6 +180,7 @@ button-style="success" label="Select image" type="button" + disabled="model.selection.length === 0" action="submit(model)"> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html index e97eabf17f..dda9334e05 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html @@ -10,7 +10,8 @@ page="page" content="content" culture="culture" - on-select-app="appChanged(app)"> + on-select-app="appChanged(app)" + on-select-app-anchor="appAnchorChanged(app, anchor)"> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html index cdacea7cf1..b76c74149d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html @@ -1,16 +1,15 @@ 
-
+
-
+
{{ group.label }}
-  
-
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html index 9d879fac22..76bca6fce2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html @@ -13,7 +13,8 @@ on-open-split-view="vm.openSplitView(variant)" on-close-split-view="vm.closeSplitView($index)" on-select-variant="vm.selectVariant(variant, $index)" - on-select-app="vm.selectApp(app)"> + on-select-app="vm.selectApp(app)" + on-select-app-anchor="vm.selectAppAnchor(app, anchor)">
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html index c03b017a82..34c7792055 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html @@ -1,19 +1,19 @@ 
- -
+
+ navigation="content.apps" + on-select="selectNavigationItem(item)" + on-anchor-select="selectAnchorItem(item, anchor)">
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html new file mode 100644 index 0000000000..90408e00cc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html @@ -0,0 +1,19 @@ + + + {{ vm.item.name }} +
{{vm.item.badge.count}}
+
+ + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html index e278a8c401..0c8f0be7a2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html @@ -1,46 +1,41 @@
    -
  • -
  • +
    + +
  • - -
    - {{ moreButton.name }} -
    +
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html index 9474e98350..b11b656c8e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html @@ -1,11 +1,12 @@
  •   + ng-class="{'icon-navigation-right': !node.expanded || node.metaData.isContainer, 'icon-navigation-down': node.expanded && !node.metaData.isContainer}" + ng-style="{'visibility': (scope.enablelistviewexpand === 'true' || node.hasChildren && (!node.metaData.isContainer || isDialog)) ? 'visible' : 'hidden'}" + ng-click="load(node)">  + {{node.name}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-dropdown.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-dropdown.html index 141793f6b1..2f24186597 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-dropdown.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-dropdown.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html index 21840cb1d1..c84e63a359 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html @@ -8,7 +8,7 @@ class="umb-layout-selector__dropdown shadow-depth-3 animated -half-second fadeIn" on-outside-click="vm.closeLayoutDropdown()"> -
    --> -
    +
    {{item.name}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js index ed3d6ded33..e801e2cc58 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js @@ -192,7 +192,6 @@ function FormsController($scope, $route, $cookies, packageResource, localization function Video_player (videoId) { // Get dom elements this.container = document.getElementById(videoId); - this.video = this.container.getElementsByTagName('video')[0]; //Create controls this.controls = document.createElement('div'); @@ -215,104 +214,6 @@ function FormsController($scope, $route, $cookies, packageResource, localization this.controls.appendChild(this.loader); this.loader.appendChild(this.progress_bar); } - - - Video_player.prototype - .seeking = function() { - // get the value of the seekbar (hidden input[type="range"]) - var time = this.video.duration * (this.seek_bar.value / 100); - - // Update video to seekbar value - this.video.currentTime = time; - }; - - // Stop video when user initiates seeking - Video_player.prototype - .start_seek = function() { - this.video.pause(); - }; - - // Start video when user stops seeking - Video_player.prototype - .stop_seek = function() { - this.video.play(); - }; - - // Update the progressbar (span.loader) according to video.currentTime - Video_player.prototype - .update_progress_bar = function() { - // Get video progress in % - var value = (100 / this.video.duration) * this.video.currentTime; - - // Update progressbar - this.progress_bar.style.width = value + '%'; - }; - - // Bind progressbar to mouse when seeking - Video_player.prototype - .handle_mouse_move = function(event) { - // Get position of progressbar relative to browser window - var pos = this.progress_bar.getBoundingClientRect().left; - - // Make sure event is reckonized cross-browser - event = event || window.event; - - // Update progressbar - this.progress_bar.style.width = (event.clientX - pos) + "px"; - }; - - // Eventlisteners for seeking - Video_player.prototype - .video_event_handler = function(videoPlayer, interval) { - // Update the progress bar - var animate_progress_bar = setInterval(function () { - videoPlayer.update_progress_bar(); - }, interval); - - // Fire when input value changes (user seeking) - videoPlayer.seek_bar - .addEventListener("change", function() { - videoPlayer.seeking(); - }); - - // Fire when user clicks on seekbar - videoPlayer.seek_bar - .addEventListener("mousedown", function (clickEvent) { - // Pause video playback - videoPlayer.start_seek(); - - // Stop updating progressbar according to video progress - clearInterval(animate_progress_bar); - - // Update progressbar to where user clicks - videoPlayer.handle_mouse_move(clickEvent); - - // Bind progressbar to cursor - window.onmousemove = function(moveEvent){ - videoPlayer.handle_mouse_move(moveEvent); - }; - }); - - // Fire when user releases seekbar - videoPlayer.seek_bar - .addEventListener("mouseup", function () { - - // Unbind progressbar from cursor - window.onmousemove = null; - - // Start video playback - videoPlayer.stop_seek(); - - // Animate the progressbar - animate_progress_bar = setInterval(function () { - videoPlayer.update_progress_bar(); - }, interval); - }); - }; - - - var videoPlayer = new Video_player('video_1'); - videoPlayer.video_event_handler(videoPlayer, 17); } angular.module("umbraco").controller("Umbraco.Dashboard.FormsDashboardController", FormsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/forms/formsdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/forms/formsdashboardintro.html index 3b382367c4..c18e7f4ccf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/forms/formsdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/forms/formsdashboardintro.html @@ -8,14 +8,6 @@
    -
    - -
    -

    Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it!

    {{ group.label }}
     
    - +
    - +
    @@ -38,8 +38,8 @@ - @@ -51,6 +51,7 @@ ng-if="page.listViewPath" type="link" href="#{{page.listViewPath}}" + button-style="link" label="Return to list" label-key="buttons_returnToList"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js index 9ef1b69aad..1e679eb5ae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js @@ -42,7 +42,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.CheckboxListContro return f.checked; }), function(m) { - return m.key; + return m.value; }); //get all of the same values between the arrays var same = _.intersection($scope.model.value, selectedVals); @@ -54,7 +54,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.CheckboxListContro $scope.selectedItems = []; for (var i = 0; i < configItems.length; i++) { - var isChecked = _.contains($scope.model.value, configItems[i].id); + var isChecked = _.contains($scope.model.value, configItems[i].value); $scope.selectedItems.push({ checked: isChecked, key: configItems[i].id, @@ -66,13 +66,13 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.CheckboxListContro function changed(item) { var index = _.findIndex($scope.model.value, function (v) { - return v === item.key; + return v === item.value; }); - + if (item.checked) { //if it doesn't exist in the model, then add it if (index < 0) { - $scope.model.value.push(item.key); + $scope.model.value.push(item.value); } } else { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.html index 29760e3f5b..7ec7067734 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.html @@ -4,9 +4,9 @@
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 15a1526b9f..16aa7efceb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -105,6 +105,10 @@ function contentPickerController($scope, entityResource, editorState, iconHelper }; if ($scope.model.config) { + //special case, if the `startNode` is falsy on the server config delete it entirely so the default value is merged in + if (!$scope.model.config.startNode) { + delete $scope.model.config.startNode; + } //merge the server config on top of the default config, then set the server config to use the result $scope.model.config = angular.extend(defaultConfig, $scope.model.config); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.controller.js new file mode 100644 index 0000000000..3441b6a060 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.controller.js @@ -0,0 +1,16 @@ +function EditConfigController($scope) { + + $scope.close = function() { + if($scope.model.close) { + $scope.model.close(); + } + } + + $scope.submit = function() { + if($scope.model && $scope.model.submit) { + $scope.model.submit($scope.model); + } + } +} + +angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.EditConfigController", EditConfigController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.html index fb541ecf84..9c42e04f75 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.html @@ -1,3 +1,25 @@ +
    + + + + + +
    + + + + + + + + + +

    {{model.name}}

    @@ -12,3 +34,34 @@
    + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js index 566535fc98..4ad857b1e7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js @@ -1,10 +1,25 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.GridPrevalueEditor.LayoutConfigController", - function ($scope) { + function ($scope, localizationService) { + + + function init() { + setTitle(); + } + + function setTitle() { + if (!$scope.model.title) { + localizationService.localize("grid_addGridLayout") + .then(function(data){ + $scope.model.title = data; + }); + } + } $scope.currentLayout = $scope.model.currentLayout; $scope.columns = $scope.model.columns; $scope.rows = $scope.model.rows; + $scope.currentSection = undefined; $scope.scaleUp = function(section, max, overflow){ var add = 1; @@ -57,9 +72,12 @@ angular.module("umbraco") template.sections.splice(index, 1); }; - $scope.closeSection = function(){ - $scope.currentSection = undefined; - }; + + $scope.close = function() { + if($scope.model.close) { + $scope.model.close(); + } + } $scope.$watch("currentLayout", function(layout){ if(layout){ @@ -71,4 +89,6 @@ angular.module("umbraco") $scope.availableLayoutSpace = $scope.columns - total; } }, true); + + init(); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html index 0e1a92a62c..4f0b665e24 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html @@ -1,8 +1,31 @@
-
-
+ + + + +
+ + + + + + + + + + + + + +
+

@@ -32,7 +55,7 @@
-
+
@@ -111,4 +134,27 @@
-
+ + + + + + + + + + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js index 4e3dde50e4..1bb7516f30 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js @@ -1,5 +1,19 @@ -function RowConfigController($scope) { +function RowConfigController($scope, localizationService) { + function init() { + setTitle(); + } + + function setTitle() { + if (!$scope.model.title) { + localizationService.localize("grid_addRowConfiguration") + .then(function(data){ + $scope.model.title = data; + }); + } + } + + $scope.currentRow = $scope.model.currentRow; $scope.editors = $scope.model.editors; $scope.columns = $scope.model.columns; @@ -69,6 +83,12 @@ function RowConfigController($scope) { $scope.closeArea = function() { $scope.currentCell = undefined; }; + + $scope.close = function() { + if($scope.model.close) { + $scope.model.close(); + } + } $scope.nameChanged = false; var originalName = $scope.currentRow.name; @@ -93,6 +113,10 @@ function RowConfigController($scope) { } }, true); + + init(); + + } angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.RowConfigController", RowConfigController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html index c9a0d807ea..10af5ade3c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html @@ -1,8 +1,30 @@
-
-
+ + + +
+ + + + + + + + + + + + + +
+

@@ -14,7 +36,7 @@ - + @@ -54,7 +76,7 @@
- + @@ -97,4 +119,27 @@
+ + + +
+
+
+ + + + + + + + +
+ +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.controller.js new file mode 100644 index 0000000000..10b5601304 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.controller.js @@ -0,0 +1,16 @@ +function DeleteRowConfirmController($scope) { + + $scope.close = function() { + if($scope.model.close) { + $scope.model.close(); + } + } + + $scope.submit = function() { + if($scope.model && $scope.model.submit) { + $scope.model.submit($scope.model); + } + } +} + +angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.DeleteRowConfirmController", DeleteRowConfirmController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html index c69ae2cb8f..2bf1f00b0e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html @@ -1,4 +1,13 @@ -
+
+ + + + + + + + +

Warning!

@@ -15,4 +24,31 @@ Are you sure?

+ + + +
+
+
+ + + + + + + + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js index 19057fa842..d38697d351 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js @@ -1,6 +1,6 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.GridPrevalueEditorController", - function ($scope, gridService) { + function ($scope, gridService, editorService) { var emptyModel = { styles:[ @@ -89,28 +89,23 @@ angular.module("umbraco") }; $scope.model.value.templates.push(template); } + + var layoutConfigOverlay = { + currentLayout: template, + rows: $scope.model.value.layouts, + columns: $scope.model.value.columns, + view: "views/propertyEditors/grid/dialogs/layoutconfig.html", + size: "small", + submit: function(model) { + editorService.close(); + }, + close: function(model) { + editorService.close(); + } + }; - $scope.layoutConfigOverlay = {}; - $scope.layoutConfigOverlay.view = "views/propertyEditors/grid/dialogs/layoutconfig.html"; - $scope.layoutConfigOverlay.currentLayout = template; - $scope.layoutConfigOverlay.rows = $scope.model.value.layouts; - $scope.layoutConfigOverlay.columns = $scope.model.value.columns; - $scope.layoutConfigOverlay.show = true; + editorService.open(layoutConfigOverlay); - $scope.layoutConfigOverlay.submit = function(model) { - $scope.layoutConfigOverlay.show = false; - $scope.layoutConfigOverlay = null; - }; - - $scope.layoutConfigOverlay.close = function(oldModel) { - - //reset templates - $scope.model.value.templates = templatesCopy; - - $scope.layoutConfigOverlay.show = false; - $scope.layoutConfigOverlay = null; - } - }; $scope.deleteTemplate = function(index){ @@ -135,50 +130,44 @@ angular.module("umbraco") }; $scope.model.value.layouts.push(layout); } - - $scope.rowConfigOverlay = {}; - $scope.rowConfigOverlay.view = "views/propertyEditors/grid/dialogs/rowconfig.html"; - $scope.rowConfigOverlay.currentRow = layout; - $scope.rowConfigOverlay.editors = $scope.editors; - $scope.rowConfigOverlay.columns = $scope.model.value.columns; - $scope.rowConfigOverlay.show = true; - - $scope.rowConfigOverlay.submit = function(model) { - $scope.rowConfigOverlay.show = false; - $scope.rowConfigOverlay = null; - }; - - $scope.rowConfigOverlay.close = function(oldModel) { - $scope.model.value.layouts = layoutsCopy; - $scope.rowConfigOverlay.show = false; - $scope.rowConfigOverlay = null; + + var rowConfigOverlay = { + currentRow: layout, + editors: $scope.editors, + columns: $scope.model.value.columns, + view: "views/propertyEditors/grid/dialogs/rowconfig.html", + size: "small", + submit: function(model) { + editorService.close(); + }, + close: function(model) { + editorService.close(); + } }; + editorService.open(rowConfigOverlay); + }; //var rowDeletesPending = false; $scope.deleteLayout = function(index) { + + var rowDeleteOverlay = { + dialogData: { + rowName: $scope.model.value.layouts[index].name + }, + view: "views/propertyEditors/grid/dialogs/rowdeleteconfirm.html", + size: "small", + submit: function(model) { + $scope.model.value.layouts.splice(index, 1); + editorService.close(); + }, + close: function(model) { + editorService.close(); + } + }; - $scope.rowDeleteOverlay = {}; - $scope.rowDeleteOverlay.view = "views/propertyEditors/grid/dialogs/rowdeleteconfirm.html"; - $scope.rowDeleteOverlay.dialogData = { - rowName: $scope.model.value.layouts[index].name - }; - $scope.rowDeleteOverlay.show = true; - - $scope.rowDeleteOverlay.submit = function(model) { - - $scope.model.value.layouts.splice(index, 1); - - $scope.rowDeleteOverlay.show = false; - $scope.rowDeleteOverlay = null; - }; - - $scope.rowDeleteOverlay.close = function(oldModel) { - $scope.rowDeleteOverlay.show = false; - $scope.rowDeleteOverlay = null; - }; - + editorService.open(rowDeleteOverlay); }; @@ -210,26 +199,22 @@ angular.module("umbraco") }; var editConfigCollection = function(configValues, title, callback) { + + var editConfigCollectionOverlay = { + config: configValues, + title: title, + view: "views/propertyeditors/grid/dialogs/editconfig.html", + size: "small", + submit: function(model) { + callback(model.config); + editorService.close(); + }, + close: function(model) { + editorService.close(); + } + }; - $scope.editConfigCollectionOverlay = {}; - $scope.editConfigCollectionOverlay.view = "views/propertyeditors/grid/dialogs/editconfig.html"; - $scope.editConfigCollectionOverlay.config = configValues; - $scope.editConfigCollectionOverlay.title = title; - $scope.editConfigCollectionOverlay.show = true; - - $scope.editConfigCollectionOverlay.submit = function(model) { - - callback(model.config); - - $scope.editConfigCollectionOverlay.show = false; - $scope.editConfigCollectionOverlay = null; - }; - - $scope.editConfigCollectionOverlay.close = function(oldModel) { - $scope.editConfigCollectionOverlay.show = false; - $scope.editConfigCollectionOverlay = null; - }; - + editorService.open(editConfigCollectionOverlay); }; $scope.editConfig = function() { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html index 539433821b..92d1a9ef26 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html @@ -158,28 +158,4 @@
- - - - - - - - - - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index a3fc33d2ea..44c0c4ae7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -142,7 +142,6 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs } $scope.options = { - displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, filter: '', diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index 9a638d91fa..2c70ba5730 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -6,7 +6,7 @@
- + @@ -85,7 +85,7 @@ type="button" label="Clear selection" label-key="buttons_clearSelection" - button-style="selection" + button-style="white" action="clearSelection()" disabled="actionInProgress"> @@ -140,7 +140,7 @@ ng-if="options.allowBulkPublish && (buttonPermissions == null || buttonPermissions.canPublish)" style="margin-right: 5px;" type="button" - button-style="selection" + button-style="white" label-key="actions_publish" icon="icon-globe" action="publish()" @@ -153,7 +153,7 @@ ng-if="options.allowBulkUnpublish && (buttonPermissions == null || buttonPermissions.canUnpublish)" style="margin-right: 5px;" type="button" - button-style="selection" + button-style="white" label-key="actions_unpublish" icon="icon-block" action="unpublish()" @@ -166,7 +166,7 @@ ng-if="options.allowBulkCopy && (buttonPermissions == null || buttonPermissions.canCopy)" style="margin-right: 5px;" type="button" - button-style="selection" + button-style="white" label-key="actions_copy" icon="icon-documents" action="copy()" @@ -179,7 +179,7 @@ ng-if="options.allowBulkMove && (buttonPermissions == null || buttonPermissions.canMove)" style="margin-right: 5px;" type="button" - button-style="selection" + button-style="white" label-key="actions_move" icon="icon-enter" action="move()" @@ -191,7 +191,7 @@ -
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 74b43eb2aa..3af5cc064a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -12,9 +12,9 @@ hide-alias="true" navigation="vm.user.navigation"> - + - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index deee402fd5..863d9658ae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -2,13 +2,13 @@ - - + + @@ -40,6 +40,7 @@ @@ -54,6 +55,7 @@ type="button" label="Delete" label-key="general_delete" + button-style="white" icon="icon-trash" action="vm.deleteUserGroups()" size="xs"> @@ -64,7 +66,7 @@
- Groups ({{vm.userGroups.length}}) + Groups ({{vm.userGroups.length}})
@@ -76,19 +78,20 @@ - - - + + + + ng-click="vm.selectUserGroup(group, vm.selection, $event)" + style="width: 20px; height: 100px;"/> - -
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 92ed10267d..38f6758651 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -59,13 +59,10 @@ } ]; - vm.activeLayout = { - "icon": "icon-thumbnails-small", - "path": "1", - "selected": true - }; + // Set card layout to active by default + vm.activeLayout = vm.layouts[0]; - //don't show the invite button if no email is configured + // Don't show the invite button if no email is configured if (Umbraco.Sys.ServerVariables.umbracoSettings.showUserInvite) { vm.defaultButton = { labelKey: "user_inviteUser", @@ -199,27 +196,23 @@ vm.activeLayout = selectedLayout; } - function selectUser(user, selection, event) { - - // prevent the current user to be selected - if (!user.isCurrentUser) { - - if (user.selected) { - var index = selection.indexOf(user.id); - selection.splice(index, 1); - user.selected = false; - } else { - user.selected = true; - vm.selection.push(user.id); - } - - setBulkActions(vm.users); - - if (event) { - event.preventDefault(); - event.stopPropagation(); - } + function selectUser(user) { + + if (user.isCurrentUser) { + return; } + + if (user.selected) { + var index = vm.selection.indexOf(user.id); + vm.selection.splice(index, 1); + user.selected = false; + } else { + user.selected = true; + vm.selection.push(user.id); + } + + setBulkActions(vm.users); + } function clearSelection() { @@ -230,11 +223,7 @@ } function clickUser(user) { - if (vm.selection.length > 0) { - selectUser(user, vm.selection); - } else { - goToUser(user.id); - } + goToUser(user.id); } function disableUsers() { @@ -628,18 +617,20 @@ var firstSelectedUserGroups; angular.forEach(users, function (user) { - + if (!user.selected) { return; } - + + // if the current user is selected prevent any bulk actions with the user included if (user.isCurrentUser) { vm.allowDisableUser = false; vm.allowEnableUser = false; vm.allowUnlockUser = false; vm.allowSetUserGroup = false; - return; + + return false; } if (user.userDisplayState && user.userDisplayState.key === "Disabled") { @@ -663,16 +654,17 @@ } // store the user group aliases of the first selected user - if (!firstSelectedUserGroups) { - firstSelectedUserGroups = user.userGroups.map(function (ug) { return ug.alias; }); - vm.allowSetUserGroup = true; - } else if (vm.allowSetUserGroup === true) { - // for 2nd+ selected user, compare the user group aliases to determine if we should allow bulk editing. - // we don't allow bulk editing of users not currently having the same assigned user groups, as we can't - // really support that in the user group picker. - var userGroups = user.userGroups.map(function (ug) { return ug.alias; }); - if (_.difference(firstSelectedUserGroups, userGroups).length > 0) { - vm.allowSetUserGroup = false; + if (vm.allowSetUserGroup === true) { + if (!firstSelectedUserGroups) { + firstSelectedUserGroups = user.userGroups.map(function (ug) { return ug.alias; }); + } else { + // for 2nd+ selected user, compare the user group aliases to determine if we should allow bulk editing. + // we don't allow bulk editing of users not currently having the same assigned user groups, as we can't + // really support that in the user group picker. + var userGroups = user.userGroups.map(function (ug) { return ug.alias; }); + if (_.difference(firstSelectedUserGroups, userGroups).length > 0) { + vm.allowSetUserGroup = false; + } } } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 4b6f77e696..cefc3c5c86 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -5,7 +5,7 @@ - + @@ -23,21 +23,20 @@ ng-if="vm.layouts" layouts="vm.layouts" active-layout="vm.activeLayout" - on-layout-select="vm.selectLayout"> + on-layout-select="vm.selectLayout(layout)"> @@ -49,7 +48,7 @@ @@ -58,16 +57,16 @@ {{ vm.selection.length }} of {{ vm.users.length }} selected - + @@ -75,22 +74,22 @@ - +
- Status: + Status: {{ vm.getFilterName(vm.userStatesFilter) }} @@ -168,7 +167,7 @@
- Order by: + Order by: {{ vm.getSortLabel(vm.usersOptions.orderBy, vm.usersOptions.orderDirection) }} @@ -192,20 +191,18 @@
- +
- + - - +
@@ -245,9 +242,14 @@
-
+
+ {{user.name}}{{user.name}} {{ userGroup.name }}, {{ user.formattedLastLogin }} @@ -332,7 +334,7 @@ Required {{addUserForm.name.errorMsg}} - + @@ -342,7 +344,7 @@ Required {{addUserForm.username.errorMsg}} - + @@ -356,7 +358,7 @@ - +
- + Back to users - +
- +

- +
@@ -511,7 +513,7 @@ - +
- + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 852abcd5d8..7dcd101aff 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1388,7 +1388,7 @@ To manage your website, simply open the Umbraco back office and start adding con Styles The CSS that should be applied in the rich text editor, e.g. "color:red;" Code - Rich Text Editor + Rich Text Editor Failed to delete template with ID %0% diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index a926a7c1d3..e300e6562e 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -6,32 +6,26 @@ "groupOrder": 100, "allowDisable": true, "requiredSections": [ - "content", - "media", - "settings", - "developer", - "users", - "member", - "forms" + "content" ], "steps": [ { "title": "Welcome to Umbraco - The Friendly CMS", - "content": "

Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible.

In this quick tour we will introduce you to the main areas of Umbraco and show you how to best get started.

If you don't want to take the tour now you can always start it by opening the Help drawer in the bottom left corner.

", + "content": "

Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible.

In this quick tour we will introduce you to the main areas of Umbraco and show you how to best get started.

If you don't want to take the tour now you can always start it by opening the Help drawer in the top right corner.

", "type": "intro" }, { "element": "[data-element='sections']", "elementPreventClick": true, "title": "Main Menu", - "content": "This is the main menu in Umbraco backoffice. Here you can navigate between the different sections, see your user profile and open the help drawer.", + "content": "This is the main menu in Umbraco backoffice. Here you can navigate between the different sections, search for items, see your user profile and open the help drawer.", "backdropOpacity": 0.6 }, { "element": "[data-element='section-content']", "elementPreventClick": true, "title": "Sections", - "content": "Each area in Umbraco is called a Section. Right now you are in the Content section, when you want to go to another section simply click on the appropriate icon in the main menu and you'll be there in no time.", + "content": "Each area in Umbraco is called a Section. Right now you are in the Content section, when you want to go to another section simply click on the appropriate name in the main menu and you'll be there in no time.", "backdropOpacity": 0.6 }, { @@ -62,7 +56,7 @@ "element": "[data-element~='overlay-user']", "elementPreventClick": true, "title": "User profile", - "content": "

Here you can see details about your user, what Umbraco version the site is running, change your password and log out of Umbraco.

In the User section you will be able to do more advanced user management.

" + "content": "

Here you can see details about your user, change your password and log out of Umbraco.

In the User section you will be able to do more advanced user management.

" }, { "element": "[data-element~='overlay-user'] [data-element='button-overlayClose']", @@ -97,13 +91,7 @@ "group": "Getting Started", "groupOrder": 100, "requiredSections": [ - "content", - "media", - "settings", - "developer", - "users", - "member", - "forms" + "settings" ], "steps": [ { @@ -150,8 +138,8 @@ }, { "element": "[data-element='group-name-field']", - "title": "Name the tab", - "content": "

Enter Home in the tab name.

You can name a tab anything you want and if you have a lot of properties it can be useful to add multiple tabs.

", + "title": "Name the group", + "content": "

Enter Home in the group name.

You can name a group anything you want and if you have a lot of properties it can be useful to add multiple groups.

", "view": "tabName" }, { @@ -233,13 +221,7 @@ "group": "Getting Started", "groupOrder": 100, "requiredSections": [ - "content", - "media", - "settings", - "developer", - "users", - "member", - "forms" + "content" ], "steps": [ { @@ -292,13 +274,7 @@ "group": "Getting Started", "groupOrder": 100, "requiredSections": [ - "content", - "media", - "settings", - "developer", - "users", - "member", - "forms" + "settings" ], "steps": [ { @@ -347,13 +323,7 @@ "group": "Getting Started", "groupOrder": 100, "requiredSections": [ - "content", - "media", - "settings", - "developer", - "users", - "member", - "forms" + "content" ], "steps": [ { @@ -376,7 +346,7 @@ "eventElement": "#tree [data-element='tree-item-Home'] a.umb-tree-item__label" }, { - "element": "[data-element='editor-content'] [data-element='sub-view-info']", + "element": "[data-element='editor-content'] [data-element='sub-view-umbInfo']", "title": "Info", "content": "

Under the Info-app you will find the default information about a content item.

", "event": "click" @@ -396,13 +366,7 @@ "group": "Getting Started", "groupOrder": 100, "requiredSections": [ - "content", - "media", - "settings", - "developer", - "users", - "member", - "forms" + "media" ], "steps": [ { @@ -451,7 +415,7 @@ { "element": "[data-element='editor-media'] [data-element='media-grid-item-0']", "title": "View media item details", - "content": "Hover over the media item and Click the purple bar to view details about the media item.", + "content": "Hover over the media item and Click the white bar to view details about the media item.", "event": "click", "eventElement": "[data-element='editor-media'] [data-element='media-grid-item-0'] [data-element='media-grid-item-edit']" }, @@ -467,9 +431,9 @@ "content": "

You will also find other details about the image, like the size.

Media items work in much the same way as content. So you can add extra properties to an image by creating or editing the Media types in the Settings section.

" }, { - "element": "[data-element='editor-media'] [data-element='sub-view-info']", + "element": "[data-element='editor-media'] [data-element='sub-view-umbInfo']", "title": "Info", - "content": "Like the content section you can also find default information about the media item. You will find these under the info tab.", + "content": "Like the content section you can also find default information about the media item. You will find these under the info app.", "event": "click" }, { diff --git a/src/Umbraco.Web/Dashboards/FormsDashboard.cs b/src/Umbraco.Web/Dashboards/FormsDashboard.cs index a3e1123369..867e8af3aa 100644 --- a/src/Umbraco.Web/Dashboards/FormsDashboard.cs +++ b/src/Umbraco.Web/Dashboards/FormsDashboard.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Dashboards; @@ -9,7 +10,7 @@ namespace Umbraco.Web.Dashboards { public string Alias => "formsInstall"; - public string[] Sections => new [] { "forms" }; + public string[] Sections => new [] { Constants.Applications.Forms }; public string View => "views/dashboard/forms/formsdashboardintro.html"; diff --git a/src/Umbraco.Web/Editors/MacroRenderingController.cs b/src/Umbraco.Web/Editors/MacroRenderingController.cs index 3cd2e86dd8..49b58416cd 100644 --- a/src/Umbraco.Web/Editors/MacroRenderingController.cs +++ b/src/Umbraco.Web/Editors/MacroRenderingController.cs @@ -124,7 +124,7 @@ namespace Umbraco.Web.Editors // When rendering the macro in the backoffice the default setting would be to use the Culture of the logged in user. // Since a Macro might contain thing thats related to the culture of the "IPublishedContent" (ie Dictionary keys) we want // to set the current culture to the culture related to the content item. This is hacky but it works. - + var culture = publishedContent.GetCulture(); _variationContextAccessor.VariationContext = new VariationContext(); //must have an active variation context! if (culture != null) @@ -156,7 +156,8 @@ namespace Umbraco.Web.Editors { Alias = macroName.ToSafeAlias(), Name = macroName, - MacroSource = model.VirtualPath.EnsureStartsWith("~") + MacroSource = model.VirtualPath.EnsureStartsWith("~"), + MacroType = MacroTypes.PartialView }; _macroService.Save(macro); // may throw diff --git a/src/Umbraco.Web/Editors/SectionController.cs b/src/Umbraco.Web/Editors/SectionController.cs index 0a2f17cd15..52034b9c95 100644 --- a/src/Umbraco.Web/Editors/SectionController.cs +++ b/src/Umbraco.Web/Editors/SectionController.cs @@ -43,7 +43,7 @@ namespace Umbraco.Web.Editors // this is a bit nasty since we'll be proxying via the app tree controller but we sort of have to do that // since tree's by nature are controllers and require request contextual data - var appTreeController = new ApplicationTreeController(GlobalSettings, UmbracoContext, SqlContext, Services, AppCaches, Logger, RuntimeState, _treeService, Umbraco) + var appTreeController = new ApplicationTreeController(GlobalSettings, UmbracoContext, SqlContext, Services, AppCaches, Logger, RuntimeState, _treeService, _sectionService, Umbraco) { ControllerContext = ControllerContext }; diff --git a/src/Umbraco.Web/Models/Mapping/MacroMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/MacroMapperProfile.cs index de649a09d8..a507cc4c03 100644 --- a/src/Umbraco.Web/Models/Mapping/MacroMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/MacroMapperProfile.cs @@ -45,9 +45,15 @@ namespace Umbraco.Web.Models.Mapping } parameter.View = paramEditor.GetValueEditor().View; - var paramConfig = paramEditor.GetConfigurationEditor().ToValueEditor(paramEditor.DefaultConfiguration); - //set the config - parameter.Configuration = paramConfig; + + // sets the parameter configuration to be the default configuration editor's configuration, + // ie configurationEditor.DefaultConfigurationObject, prepared for the value editor, ie + // after ToValueEditor - important to use DefaultConfigurationObject here, because depending + // on editors, ToValueEditor expects the actual strongly typed configuration - not the + // dictionary thing returned by DefaultConfiguration + + var configurationEditor = paramEditor.GetConfigurationEditor(); + parameter.Configuration = configurationEditor.ToValueEditor(configurationEditor.DefaultConfigurationObject); }); } } diff --git a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs index 7f8dc3f03a..e0f7184dc7 100644 --- a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs @@ -8,11 +8,6 @@ namespace Umbraco.Web.PropertyEditors /// /// A property editor to allow multiple checkbox selection of pre-defined items. /// - /// - /// Due to remaining backwards compatible, this stores the id of the checkbox items in the database - /// as INT and we have logic in here to ensure it is formatted correctly including ensuring that the string value is published - /// in cache and not the int ID. - /// [DataEditor(Constants.PropertyEditors.Aliases.CheckBoxList, "Checkbox list", "checkboxlist", Icon="icon-bulleted-list", Group="lists")] public class CheckBoxListPropertyEditor : DataEditor { @@ -31,6 +26,6 @@ namespace Umbraco.Web.PropertyEditors protected override IConfigurationEditor CreateConfigurationEditor() => new ValueListConfigurationEditor(_textService); /// - protected override IDataValueEditor CreateValueEditor() => new PublishValuesMultipleValueEditor(Logger, Attribute); + protected override IDataValueEditor CreateValueEditor() => new MultipleValueEditor(Logger, Attribute); } } diff --git a/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs index 4424e1d245..eac692fcdd 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() { - return new PublishValuesMultipleValueEditor(Logger, Attribute); + return new MultipleValueEditor(Logger, Attribute); } protected override IConfigurationEditor CreateConfigurationEditor() => new DropDownFlexibleConfigurationEditor(_textService); diff --git a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs index 6448a3354c..bcddc18369 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs @@ -13,7 +13,6 @@ namespace Umbraco.Web.PropertyEditors // initialize defaults PageSize = 10; - DisplayAtTabNumber = 1; OrderBy = "SortOrder"; OrderDirection = "asc"; @@ -43,9 +42,6 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("pageSize", "Page Size", "number", Description = "Number of items per page")] public int PageSize { get; set; } - [ConfigurationField("displayAtTabNumber", "Display At Tab Number", "number", Description = "Which tab position that the list of child items will be displayed")] - public int DisplayAtTabNumber { get; set; } - [ConfigurationField("orderBy", "Order By", "views/propertyeditors/listview/sortby.prevalues.html", Description = "The default sort order for the list")] public string OrderBy { get; set; } @@ -64,7 +60,7 @@ namespace Umbraco.Web.PropertyEditors Description = "The bulk actions that are allowed from the list view")] public BulkActionPermissionSettings BulkActionPermissions { get; set; } = new BulkActionPermissionSettings(); // TODO: managing defaults? - [ConfigurationField("tabName", "Tab Name", "textstring", Description = "The name of the listview tab (default if empty: 'Child Items')")] + [ConfigurationField("tabName", "Content app name", "textstring", Description = "The name of the listview content app (default if empty: 'Child Items')")] public string TabName { get; set; } public class Property diff --git a/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultipleValueEditor.cs similarity index 69% rename from src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs rename to src/Umbraco.Web/PropertyEditors/MultipleValueEditor.cs index 8e0737dedd..bbeaff184e 100644 --- a/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultipleValueEditor.cs @@ -1,33 +1,32 @@ using System; using System.Linq; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; -using Umbraco.Web.Composing; namespace Umbraco.Web.PropertyEditors { /// - /// Custom value editor to handle posted json data and to return json data for the multiple selected items + /// A value editor to handle posted json array data and to return array data for the multiple selected csv items /// /// /// This is re-used by editors such as the multiple drop down list or check box list /// - internal class PublishValuesMultipleValueEditor : DataValueEditor + internal class MultipleValueEditor : DataValueEditor { private readonly ILogger _logger; - internal PublishValuesMultipleValueEditor(ILogger logger, DataEditorAttribute attribute) + internal MultipleValueEditor(ILogger logger, DataEditorAttribute attribute) : base(attribute) { _logger = logger; } /// - /// Override so that we can return a json array to the editor for multi-select values + /// Override so that we can return an array to the editor for multi-select values /// /// /// @@ -36,8 +35,8 @@ namespace Umbraco.Web.PropertyEditors /// public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { - var delimited = base.ToEditor(property, dataTypeService, culture, segment).ToString(); - return delimited.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var json = base.ToEditor(property, dataTypeService, culture, segment).ToString(); + return JsonConvert.DeserializeObject(json) ?? Array.Empty(); } /// @@ -55,9 +54,9 @@ namespace Umbraco.Web.PropertyEditors return null; } - var values = json.Select(item => item.Value()).ToList(); - //change to delimited - return string.Join(",", values); + var values = json.Select(item => item.Value()).ToArray(); + + return JsonConvert.SerializeObject(values); } } } diff --git a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs index 46069fec79..601728189c 100644 --- a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.PropertyEditors /// /// A property editor to allow the individual selection of pre-defined items. /// - [DataEditor(Constants.PropertyEditors.Aliases.RadioButtonList, "Radio button list", "radiobuttons", ValueType = ValueTypes.Integer, Group="lists", Icon="icon-target")] + [DataEditor(Constants.PropertyEditors.Aliases.RadioButtonList, "Radio button list", "radiobuttons", ValueType = ValueTypes.String, Group="lists", Icon="icon-target")] public class RadioButtonsPropertyEditor : DataEditor { private readonly ILocalizedTextService _textService; diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaConfiguration.cs b/src/Umbraco.Web/PropertyEditors/TextAreaConfiguration.cs index e00252b076..86a4184307 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaConfiguration.cs @@ -8,9 +8,9 @@ namespace Umbraco.Web.PropertyEditors public class TextAreaConfiguration { [ConfigurationField("maxChars", "Maximum allowed characters", "number", Description = "If empty - no character limit")] - public int MaxChars { get; set; } + public int? MaxChars { get; set; } [ConfigurationField("rows", "Number of rows", "number", Description = "If empty - 10 rows would be set as the default value")] - public int Rows { get; set; } + public int? Rows { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/TextAreaConfigurationEditor.cs index d02a222590..210a571c17 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaConfigurationEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaConfigurationEditor.cs @@ -5,6 +5,6 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents the configuration editor for the textarea value editor. /// - public class TextAreaConfigurationEditor : ConfigurationEditor + public class TextAreaConfigurationEditor : ConfigurationEditor { } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs index d470f54662..43add34327 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; @@ -17,8 +18,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) { - return source?.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim()).ToArray() - ?? Enumerable.Empty(); + if(source == null) return Array.Empty(); + + + return JsonConvert.DeserializeObject(source.ToString()) ?? Array.Empty(); } public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 9845becb45..cc1216eb68 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -192,10 +192,13 @@ namespace Umbraco.Web.PublishedCache.NuCache LoadCaches(); + Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default; + int GetId(ContentStore store, Guid uid) => store.LiveSnapshot.Get(uid)?.Id ?? default; + if (idkMap != null) { - idkMap.SetMapper(UmbracoObjectTypes.Document, id => _contentStore.LiveSnapshot.Get(id).Uid, uid => _contentStore.LiveSnapshot.Get(uid).Id); - idkMap.SetMapper(UmbracoObjectTypes.Media, id => _mediaStore.LiveSnapshot.Get(id).Uid, uid => _mediaStore.LiveSnapshot.Get(uid).Id); + idkMap.SetMapper(UmbracoObjectTypes.Document, id => GetUid(_contentStore, id), uid => GetId(_contentStore, uid)); + idkMap.SetMapper(UmbracoObjectTypes.Media, id => GetUid(_mediaStore, id), uid => GetId(_mediaStore, uid)); } } diff --git a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs index c49dffd6e4..acbd022f2e 100644 --- a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs @@ -38,16 +38,24 @@ namespace Umbraco.Web.Routing { get { - var oldRoutes = - (Dictionary) UmbracoContext.Current.HttpContext.Items[ - ContextKey3]; + var oldRoutes = (Dictionary) UmbracoContext.Current.HttpContext.Items[ContextKey3]; if (oldRoutes == null) - UmbracoContext.Current.HttpContext.Items[ContextKey3] = - oldRoutes = new Dictionary(); + UmbracoContext.Current.HttpContext.Items[ContextKey3] = oldRoutes = new Dictionary(); return oldRoutes; } } + private static bool HasOldRoutes + { + get + { + if (Current.UmbracoContext == null) return false; + if (Current.UmbracoContext.HttpContext == null) return false; + if (Current.UmbracoContext.HttpContext.Items[ContextKey3] == null) return false; + return true; + } + } + private static bool LockedEvents { get => Moving && UmbracoContext.Current.HttpContext.Items[ContextKey2] != null; @@ -97,8 +105,8 @@ namespace Umbraco.Web.Routing ContentService.Published += ContentService_Published; ContentService.Moving += ContentService_Moving; ContentService.Moved += ContentService_Moved; - ContentCacheRefresher.CacheUpdated += ContentCacheRefresher_CacheUpdated; + ContentCacheRefresher.CacheUpdated += ContentCacheRefresher_CacheUpdated; // kill all redirects once a content is deleted //ContentService.Deleted += ContentService_Deleted; @@ -111,21 +119,26 @@ namespace Umbraco.Web.Routing public void Terminate() { } - private static void ContentCacheRefresher_CacheUpdated(ContentCacheRefresher sender, - CacheRefresherEventArgs args) + private static void ContentCacheRefresher_CacheUpdated(ContentCacheRefresher sender, CacheRefresherEventArgs args) { + // that event is a distributed even that triggers on all nodes + // BUT it should totally NOT run on nodes other that the one that handled the other events + // and besides, it cannot run on a background thread! + if (!HasOldRoutes) + return; + // sanity checks if (args.MessageType != MessageType.RefreshByPayload) { throw new InvalidOperationException("ContentCacheRefresher MessageType should be ByPayload."); } + if (args.MessageObject == null) { return; } - var payloads = args.MessageObject as ContentCacheRefresher.JsonPayload[]; - if (payloads == null) + if (!(args.MessageObject is ContentCacheRefresher.JsonPayload[])) { throw new InvalidOperationException("ContentCacheRefresher MessageObject should be JsonPayload[]."); } @@ -137,8 +150,7 @@ namespace Umbraco.Web.Routing { // assuming we cannot have 'CacheUpdated' for only part of the infos else we'd need // to set a flag in 'Published' to indicate which entities have been refreshed ok - CreateRedirect(oldRoute.Key.ContentId, oldRoute.Key.Culture, oldRoute.Value.ContentKey, - oldRoute.Value.OldRoute); + CreateRedirect(oldRoute.Key.ContentId, oldRoute.Key.Culture, oldRoute.Value.ContentKey, oldRoute.Value.OldRoute); removeKeys.Add(oldRoute.Key); } diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComposer.cs b/src/Umbraco.Web/Runtime/WebRuntimeComposer.cs index 3afe6aa397..52373184b4 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComposer.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComposer.cs @@ -220,6 +220,7 @@ namespace Umbraco.Web.Runtime .Append() .Append() .Append() + .Append() .Append(); // register core CMS dashboards and 3rd party types - will be ordered by weight attribute & merged with package.manifest dashboards diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 162d001e96..b8fe709738 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -32,13 +32,15 @@ namespace Umbraco.Web.Trees public class ApplicationTreeController : UmbracoAuthorizedApiController { private readonly ITreeService _treeService; + private readonly ISectionService _sectionService; public ApplicationTreeController(IGlobalSettings globalSettings, UmbracoContext umbracoContext, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, - IRuntimeState runtimeState, ITreeService treeService, UmbracoHelper umbracoHelper) + IRuntimeState runtimeState, ITreeService treeService, ISectionService sectionService, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContext, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { _treeService = treeService; + _sectionService = sectionService; } /// @@ -56,12 +58,21 @@ namespace Umbraco.Web.Trees if (string.IsNullOrEmpty(application)) throw new HttpResponseException(HttpStatusCode.NotFound); + var section = _sectionService.GetByAlias(application); + if (section == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + //find all tree definitions that have the current application alias var groupedTrees = _treeService.GetBySectionGrouped(application, use); var allTrees = groupedTrees.Values.SelectMany(x => x).ToList(); if (allTrees.Count == 0) - throw new HttpResponseException(HttpStatusCode.NotFound); + { + //if there are no trees defined for this section but the section is defined then we can have a simple + //full screen section without trees + var name = Services.TextService.Localize("sections/" + application); + return TreeRootNode.CreateSingleTreeRoot(Constants.System.Root.ToInvariantString(), null, null, name, TreeNodeCollection.Empty, true); + } // handle request for a specific tree / or when there is only one tree if (!tree.IsNullOrWhiteSpace() || allTrees.Count == 1) @@ -101,8 +112,8 @@ namespace Umbraco.Web.Trees return treeRootNode; } - // otherwise it's a section with no tree, aka a fullscreen section - // todo is this true? what if we just failed to TryGetRootNode on all of them? + // otherwise it's a section with all empty trees, aka a fullscreen section + // todo is this true? what if we just failed to TryGetRootNode on all of them? SD: Yes it's true but we should check the result of TryGetRootNode and throw? return TreeRootNode.CreateSingleTreeRoot(Constants.System.Root.ToInvariantString(), null, null, name, TreeNodeCollection.Empty, true); } diff --git a/src/Umbraco.Web/Trees/FormsBackOfficeSection.cs b/src/Umbraco.Web/Trees/FormsBackOfficeSection.cs new file mode 100644 index 0000000000..048ed47f11 --- /dev/null +++ b/src/Umbraco.Web/Trees/FormsBackOfficeSection.cs @@ -0,0 +1,14 @@ +using Umbraco.Core; +using Umbraco.Core.Models.Trees; + +namespace Umbraco.Web.Trees +{ + /// + /// Defines the back office media section + /// + public class FormsBackOfficeSection : IBackOfficeSection + { + public string Alias => Constants.Applications.Forms; + public string Name => "Forms"; + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8699540e4b..26624ee4a2 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,7 +63,7 @@ - + 2.6.2.25 @@ -214,6 +214,7 @@ + @@ -860,7 +861,7 @@ - +