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/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index ef55048122..7f7229ccd6 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -69,7 +69,7 @@ namespace Umbraco.Core.Migrations.Install if (tableName.Equals(Constants.DatabaseSchema.Tables.RelationType)) CreateRelationTypeData(); - + if (tableName.Equals(Constants.DatabaseSchema.Tables.KeyValue)) CreateKeyValueData(); @@ -210,12 +210,12 @@ namespace Umbraco.Core.Migrations.Install private void CreatePropertyTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); //membership property types 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 d185c31a2b..ddc4289718 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -127,9 +127,8 @@ namespace Umbraco.Core.Migrations.Upgrade To("{ED28B66A-E248-4D94-8CDB-9BDF574023F0}"); To("{38C809D5-6C34-426B-9BEA-EFD39162595C}"); To("{6017F044-8E70-4E10-B2A3-336949692ADD}"); - To("{940FD19A-00A8-4D5C-B8FF-939143585726}"); - To("{C62C9BF1-833E-4866-B959-C8AB59E43E51}"); - + To("98339BEF-E4B2-48A8-B9D1-D173DC842BBE"); + To("{940FD19A-00A8-4D5C-B8FF-939143585726}"); //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/RadioButtonPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioButtonAndCheckboxPropertyEditorsMigration.cs similarity index 54% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioButtonPropertyEditorsMigration.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioButtonAndCheckboxPropertyEditorsMigration.cs index e032cdea43..ec60a22ecd 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioButtonPropertyEditorsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioButtonAndCheckboxPropertyEditorsMigration.cs @@ -1,81 +1,98 @@ using System; using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Sync; -using Umbraco.Web.Cache; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { - public class RadioButtonPropertyEditorsMigration : MigrationBase + public class RadioButtonAndCheckboxPropertyEditorsMigration : MigrationBase { - public RadioButtonPropertyEditorsMigration(IMigrationContext context) + public RadioButtonAndCheckboxPropertyEditorsMigration(IMigrationContext context) : base(context) { } public override void Migrate() { - //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.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList))); + MigrateRadioButtons(); + MigrateCheckBoxes(); + } + + private void MigrateCheckBoxes() + { + //fixme: complete this + + var dataTypes = GetDataTypes(Constants.PropertyEditors.Aliases.CheckBoxList); + + + } + + private void MigrateRadioButtons() + { + var dataTypes = GetDataTypes(Constants.PropertyEditors.Aliases.RadioButtonList); var refreshCache = false; foreach (var dataType in dataTypes) { ValueListConfiguration config; - if (!dataType.Configuration.IsNullOrWhiteSpace()) + if (dataType.Configuration.IsNullOrWhiteSpace()) + continue; + + // parse configuration, and update everything accordingly + try { - // parse configuration, and update everything accordingly - try - { - config = (ValueListConfiguration) new ValueListConfigurationEditor().FromDatabase( - dataType.Configuration); - } - catch (Exception ex) - { - Logger.Error( - ex, - "Invalid drop down 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 => UpdatePropertyDataDto(x, config)); - - // persist changes - foreach (var propertyDataDto in updatedDtos) Database.Update(propertyDataDto); - - UpdateDataType(dataType); - refreshCache = true; + 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 => UpdatePropertyDataDto(x, config)); + + // persist changes + foreach (var propertyDataDto in updatedDtos) Database.Update(propertyDataDto); + + UpdateDataType(dataType); + refreshCache = true; } if (refreshCache) { //FIXME: trigger cache rebuild. Currently the data in the database tables is wrong. } + } + 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) @@ -123,7 +140,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 if (!canConvert) return false; - propData.VarcharValue = values.FirstOrDefault() ?? string.Empty; + //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 = values.Count > 0 ? values[0] : string.Empty; propData.TextValue = null; propData.IntegerValue = null; return true; @@ -135,7 +153,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 private int[] ConvertStringValues(string val) { - var splitVals = new []{ val.Trim() }; + var splitVals = val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var intVals = splitVals .Select(x => int.TryParse(x, out var i) ? i : int.MinValue) diff --git a/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs b/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs index adbb0d9a39..54142a7527 100644 --- a/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs +++ b/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs @@ -14,14 +14,5 @@ /// Gets or sets the path of the entity. /// public string Path { get; set; } - - /// - /// Proxy of the Id - /// - public int NodeId - { - get => Id; - set => Id = value; - } } } 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/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/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 85912694f0..ae907051ca 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -69,7 +69,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var page = Database.Page(pageIndex + 1, pageSize, sql); var dtos = page.Items; var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); - + if (isContent) BuildVariants(entities.Cast()); @@ -198,7 +198,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IEnumerable PerformGetAllPaths(Guid objectType, Action> filter = null) { - var sql = Sql().Select(x => x.NodeId, x => x.Path).From().Where(x => x.NodeObjectType == objectType); + // NodeId is named Id on TreeEntityPath = use an alias + 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); } @@ -405,7 +406,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) .WhereIn(x => x.VersionId, versionIds) .OrderBy(x => x.VersionId); - } + } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version 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 da6c4a672c..18cebfaa57 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -52,9 +52,6 @@ - - ..\Umbraco.Tests\bin\Debug\Umbraco.Web.dll - @@ -371,6 +368,7 @@ + @@ -389,7 +387,7 @@ - + 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.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index e88aa5ae61..e0ad304c4f 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -80,7 +80,7 @@ - + 1.8.14 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/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/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/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/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()"> -