diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index 65d7855dc7..c26e6d403a 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -8,7 +8,139 @@ namespace Umbraco.Core.Configuration.Models public class ContentSettings { private const string DefaultPreviewBadge = - @"
Preview modeClick to end
"; + @" +
+ Preview mode + + … + + + Click to end preview mode + +
+ + "; public ContentNotificationSettings Notifications { get; set; } = new ContentNotificationSettings(); diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index d0ae5550ca..5d059d8a23 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -17,6 +17,11 @@ /// The preview cookie name /// public const string PreviewCookieName = "UMB_PREVIEW"; + /// + + /// Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. + /// + public const string AcceptPreviewCookieName = "UMB-WEBSITE-PREVIEW-ACCEPT"; public const string InstallerCookieName = "umb_installId"; diff --git a/src/Umbraco.Core/Services/IIconService.cs b/src/Umbraco.Core/Services/IIconService.cs index 72174a0504..963edb22a5 100644 --- a/src/Umbraco.Core/Services/IIconService.cs +++ b/src/Umbraco.Core/Services/IIconService.cs @@ -9,13 +9,13 @@ namespace Umbraco.Core.Services /// Gets an IconModel containing the icon name and SvgString according to an icon name found at the global icons path /// /// - /// + /// IconModel GetIcon(string iconName); /// /// Gets a list of all svg icons found at at the global icons path. /// - /// A list of + /// IList GetAllIcons(); } } diff --git a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs index 8b34c3315a..2bde06d9b8 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs @@ -57,6 +57,12 @@ namespace Umbraco.Examine _parentId = parentId; } + public override bool IsRegistered(IUmbracoContentIndex index) + { + // check if it should populate based on published values + return _publishedValuesOnly == index.PublishedValuesOnly; + } + protected override void PopulateIndexes(IReadOnlyList indexes) { if (indexes.Count == 0) return; @@ -69,31 +75,89 @@ namespace Umbraco.Examine { contentParentId = _parentId.Value; } + + if (_publishedValuesOnly) + { + IndexPublishedContent(contentParentId, pageIndex, pageSize, indexes); + } + else + { + IndexAllContent(contentParentId, pageIndex, pageSize, indexes); + } + } + + protected void IndexAllContent(int contentParentId, int pageIndex, int pageSize, IReadOnlyList indexes) + { IContent[] content; do { - if (!_publishedValuesOnly) - { - content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _).ToArray(); - } - else - { - //add the published filter - //note: We will filter for published variants in the validator - content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _, - _publishedQuery, Ordering.By("Path", Direction.Ascending)).ToArray(); - } + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _).ToArray(); if (content.Length > 0) { + var valueSets = _contentValueSetBuilder.GetValueSets(content).ToList(); + // ReSharper disable once PossibleMultipleEnumeration foreach (var index in indexes) - index.IndexItems(_contentValueSetBuilder.GetValueSets(content)); + { + index.IndexItems(valueSets); + } + } + + pageIndex++; + } while (content.Length == pageSize); + } + + protected void IndexPublishedContent(int contentParentId, int pageIndex, int pageSize, + IReadOnlyList indexes) + { + IContent[] content; + + var publishedPages = new HashSet(); + + do + { + //add the published filter + //note: We will filter for published variants in the validator + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _, _publishedQuery, + Ordering.By("Path", Direction.Ascending)).ToArray(); + + + if (content.Length > 0) + { + var indexableContent = new List(); + + foreach (var item in content) + { + if (item.Level == 1) + { + // first level pages are always published so no need to filter them + indexableContent.Add(item); + publishedPages.Add(item.Id); + } + else + { + if (publishedPages.Contains(item.ParentId)) + { + // only index when parent is published + publishedPages.Add(item.Id); + indexableContent.Add(item); + } + } + } + + var valueSets = _contentValueSetBuilder.GetValueSets(indexableContent.ToArray()).ToList(); + + // ReSharper disable once PossibleMultipleEnumeration + foreach (var index in indexes) + index.IndexItems(valueSets); } pageIndex++; } while (content.Length == pageSize); } } + + } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs index 3181ff663e..cc85c1eda0 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs @@ -2,8 +2,11 @@ using Examine; namespace Umbraco.Examine { + /// + /// Marker interface for indexes of Umbraco content + /// public interface IUmbracoContentIndex : IIndex { - + bool PublishedValuesOnly { get; } } } diff --git a/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs index f9d4d85dc8..bfd757f9be 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs @@ -13,9 +13,16 @@ namespace Umbraco.Examine { public override bool IsRegistered(IIndex index) { - if (base.IsRegistered(index)) return true; - return index is TIndex; + if (base.IsRegistered(index)) + return true; + + if (!(index is TIndex casted)) + return false; + + return IsRegistered(casted); } + + public virtual bool IsRegistered(TIndex index) => true; } public abstract class IndexPopulator : IIndexPopulator diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index 16f4a8bc25..bad037e241 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -615,6 +615,8 @@ namespace Umbraco.Core.Packaging var defaultTemplateElement = infoElement.Element("DefaultTemplate"); contentType.Name = infoElement.Element("Name").Value; + if (infoElement.Element("Key") != null) + contentType.Key = new Guid(infoElement.Element("Key").Value); contentType.Icon = infoElement.Element("Icon").Value; contentType.Thumbnail = infoElement.Element("Thumbnail").Value; contentType.Description = infoElement.Element("Description").Value; @@ -811,6 +813,8 @@ namespace Umbraco.Core.Packaging ? (ContentVariation)Enum.Parse(typeof(ContentVariation), property.Element("Variations").Value) : ContentVariation.Nothing }; + if (property.Element("Key") != null) + propertyType.Key = new Guid(property.Element("Key").Value); var tab = (string)property.Element("Tab"); if (string.IsNullOrEmpty(tab)) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index a0a26ea428..b8dfc4d05f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -988,6 +988,81 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } + /// + /// Inserts property values for the content entity + /// + /// + /// + /// + /// + /// + /// Used when creating a new entity + /// + protected void InsertPropertyValues(TEntity entity, int publishedVersionId, out bool edited, out HashSet editedCultures) + { + // persist the property data + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); + foreach (var propertyDataDto in propertyDataDtos) + { + Database.Insert(propertyDataDto); + } + // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs. + // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases. + } + + /// + /// Used to atomically replace the property values for the entity version specified + /// + /// + /// + /// + /// + /// + + protected void ReplacePropertyValues(TEntity entity, int versionId, int publishedVersionId, out bool edited, out HashSet editedCultures) + { + // Replace the property data. + // Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we need to be atomic + // and handle DB concurrency. Doing a clear and then re-insert is prone to concurrency issues. + + var propDataSql = SqlContext.Sql().Select("*").From().Where(x => x.VersionId == versionId).ForUpdate(); + var existingPropData = Database.Fetch(propDataSql); + var propertyTypeToPropertyData = new Dictionary<(int propertyTypeId, int versionId, int? languageId, string segment), PropertyDataDto>(); + var existingPropDataIds = new List(); + foreach (var p in existingPropData) + { + existingPropDataIds.Add(p.Id); + propertyTypeToPropertyData[(p.PropertyTypeId, p.VersionId, p.LanguageId, p.Segment)] = p; + } + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); + + foreach (var propertyDataDto in propertyDataDtos) + { + + // Check if this already exists and update, else insert a new one + if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out var propData)) + { + propertyDataDto.Id = propData.Id; + Database.Update(propertyDataDto); + } + else + { + // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs. + // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases. + Database.Insert(propertyDataDto); + } + + // track which ones have been processed + existingPropDataIds.Remove(propertyDataDto.Id); + } + // For any remaining that haven't been processed they need to be deleted + if (existingPropDataIds.Count > 0) + { + Database.Execute(SqlContext.Sql().Delete().WhereIn(x => x.Id, existingPropDataIds)); + } + + } + private class NodeIdKey { [Column("id")] diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 7e19c1aef4..3a256afe1a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -625,17 +625,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(documentVersionDto); } - // replace the property data (rather than updating) + // replace the property data (rather than updating) // only need to delete for the version that existed, the new version (if any) has no property data yet - var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; - var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deletePropertyDataSql); - - // insert property data - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0, - entity.Properties, LanguageRepository, out var edited, out var editedCultures); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); + var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; + // insert property data + ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, out var editedCultures); // if !publishing, we may have a new name != current publish name, // also impacts 'edited' diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index 9f47fd2f6b..cf4b4f20c3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -294,9 +294,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(mediaVersionDto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); + InsertPropertyValues(entity, 0, out _, out _); // set tags SetEntityTags(entity, _tagRepository); @@ -359,11 +357,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Update(mediaVersionDto); // replace the property data - var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == entity.VersionId); - Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); + ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); SetEntityTags(entity, _tagRepository); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 5b1aba12ca..77dcee45f8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -246,8 +246,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { entity.AddingEntity(); - var member = (Member) entity; - // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? entity.SanitizeEntityPropertiesForXmlStorage(); @@ -305,7 +303,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = true; Database.Insert(contentVersionDto); - member.VersionId = contentVersionDto.Id; + entity.VersionId = contentVersionDto.Id; // persist the member dto dto.NodeId = nodeDto.NodeId; @@ -322,9 +320,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(member.ContentType.Variations, member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); + InsertPropertyValues(entity, 0, out _, out _); SetEntityTags(entity, _tagRepository); @@ -336,11 +332,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } protected override void PersistUpdatedItem(IMember entity) - { - var member = (Member) entity; - + { // update - member.UpdatingEntity(); + entity.UpdatingEntity(); // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? @@ -386,27 +380,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (changedCols.Count > 0) Database.Update(dto, changedCols); - // Replace the property data - // Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we have another method that doesn't take an explicit WriteLock - // in SetLastLogin which is called very often and we want to avoid the lock timeout for the explicit lock table but we still need to ensure atomic - // operations between that method and this one. - - var propDataSql = SqlContext.Sql().Select("*").From().Where(x => x.VersionId == member.VersionId).ForUpdate(); - var existingPropData = Database.Fetch(propDataSql).ToDictionary(x => x.PropertyTypeId); - var propertyDataDtos = PropertyFactory.BuildDtos(member.ContentType.Variations, member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); - foreach (var propertyDataDto in propertyDataDtos) - { - // Check if this already exists and update, else insert a new one - if (existingPropData.TryGetValue(propertyDataDto.PropertyTypeId, out var propData)) - { - propertyDataDto.Id = propData.Id; - Database.Update(propertyDataDto); - } - else - { - Database.Insert(propertyDataDto); - } - } + ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); SetEntityTags(entity, _tagRepository); diff --git a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs index 46332aa730..e866fac560 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -255,6 +255,14 @@ namespace Umbraco.Web.Search DeleteIndexForEntity(c4.Id, false); } break; + case MessageType.RefreshByPayload: + var payload = (MemberCacheRefresher.JsonPayload[])args.MessageObject; + var members = payload.Select(x => _services.MemberService.GetById(x.Id)); + foreach(var m in members) + { + ReIndexForMember(m); + } + break; case MessageType.RefreshAll: case MessageType.RefreshByJson: default: diff --git a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs index d69297eb57..cb9f802bc8 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs @@ -441,6 +441,7 @@ namespace Umbraco.Core.Services.Implement var info = new XElement("Info", new XElement("Name", contentType.Name), new XElement("Alias", contentType.Alias), + new XElement("Key", contentType.Key), new XElement("Icon", contentType.Icon), new XElement("Thumbnail", contentType.Thumbnail), new XElement("Description", contentType.Description), @@ -491,8 +492,9 @@ namespace Umbraco.Core.Services.Implement var genericProperty = new XElement("GenericProperty", new XElement("Name", propertyType.Name), new XElement("Alias", propertyType.Alias), + new XElement("Key", propertyType.Key), new XElement("Type", propertyType.PropertyEditorAlias), - new XElement("Definition", definition.Key), + new XElement("Definition", definition.Key), new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name), new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index c35a166fb7..9a131198ab 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -35,6 +35,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Umbraco.Core.Logging.Serilog; using Umbraco.Infrastructure.Composing; +using Umbraco.Web.PublishedCache; using ConnectionStrings = Umbraco.Core.Configuration.Models.ConnectionStrings; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -87,7 +88,6 @@ namespace Umbraco.Tests.Integration.Testing var app = new ApplicationBuilder(host.Services); Configure(app); //Takes around 200 ms - OnFixtureTearDown(() => host.Dispose()); } @@ -428,7 +428,7 @@ namespace Umbraco.Tests.Integration.Testing #endregion #region Common services - + protected virtual T GetRequiredService() => Services.GetRequiredService(); public Dictionary InMemoryConfiguration { get; } = new Dictionary(); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs index 36b5e91f34..9082473f28 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs @@ -1504,6 +1504,56 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services Assert.That(content.HasIdentity, Is.True); } + [Test] + public void Can_Update_Content_Property_Values() + { + var template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + IContentType contentType = ContentTypeBuilder.CreateSimpleContentType(defaultTemplateId: template.Id); + ContentTypeService.Save(contentType); + IContent content = ContentBuilder.CreateSimpleContent(contentType, "hello"); + content.SetValue("title", "title of mine"); + content.SetValue("bodyText", "hello world"); + ContentService.SaveAndPublish(content); + + // re-get + content = ContentService.GetById(content.Id); + content.SetValue("title", "another title of mine"); // Change a value + content.SetValue("bodyText", null); // Clear a value + content.SetValue("author", "new author"); // Add a value + ContentService.SaveAndPublish(content); + + // re-get + content = ContentService.GetById(content.Id); + Assert.AreEqual("another title of mine", content.GetValue("title")); + Assert.IsNull(content.GetValue("bodyText")); + Assert.AreEqual("new author", content.GetValue("author")); + + content.SetValue("title", "new title"); + content.SetValue("bodyText", "new body text"); + content.SetValue("author", "new author text"); + ContentService.Save(content); // new non-published version + + // re-get + content = ContentService.GetById(content.Id); + content.SetValue("title", null); // Clear a value + content.SetValue("bodyText", null); // Clear a value + ContentService.Save(content); // saving non-published version + + // re-get + content = ContentService.GetById(content.Id); + Assert.IsNull(content.GetValue("title")); // Test clearing the value worked with the non-published version + Assert.IsNull(content.GetValue("bodyText")); + Assert.AreEqual("new author text", content.GetValue("author")); + + // make sure that the published version remained the same + var publishedContent = ContentService.GetVersion(content.PublishedVersionId); + Assert.AreEqual("another title of mine", publishedContent.GetValue("title")); + Assert.IsNull(publishedContent.GetValue("bodyText")); + Assert.AreEqual("new author", publishedContent.GetValue("author")); + } + [Test] public void Can_Bulk_Save_Content() { diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs index 84b9af3951..ecb3292727 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Services; using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Integration.Testing; using Umbraco.Tests.Testing; +using Umbraco.Web.PublishedCache; namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { @@ -39,6 +40,10 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [SetUp] public void SetupTestData() { + + //This is super nasty, but this lets us initialize the cache while it is empty. + _ = GetRequiredService(); + if (_langFr == null && _langEs == null) { var globalSettings = new GlobalSettings(); @@ -663,7 +668,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [Test] public void EntityService_Can_Get_Key_For_Id_With_Unknown_Type() { - var result = EntityService.GetKey(1052, UmbracoObjectTypes.Unknown); + var result = EntityService.GetKey(_contentType.Id, UmbracoObjectTypes.Unknown); Assert.IsTrue(result.Success); Assert.AreEqual(Guid.Parse("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"), result.Result); @@ -672,7 +677,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [Test] public void EntityService_Can_Get_Key_For_Id() { - var result = EntityService.GetKey(1052, UmbracoObjectTypes.DocumentType); + var result = EntityService.GetKey(_contentType.Id, UmbracoObjectTypes.DocumentType); Assert.IsTrue(result.Success); Assert.AreEqual(Guid.Parse("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"), result.Result); @@ -681,8 +686,8 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [Test] public void EntityService_Cannot_Get_Key_For_Id_With_Incorrect_Object_Type() { - var result1 = EntityService.GetKey(1052, UmbracoObjectTypes.DocumentType); - var result2 = EntityService.GetKey(1052, UmbracoObjectTypes.MediaType); + var result1 = EntityService.GetKey(_contentType.Id, UmbracoObjectTypes.DocumentType); + var result2 = EntityService.GetKey(_contentType.Id, UmbracoObjectTypes.MediaType); Assert.IsTrue(result1.Success); Assert.IsFalse(result2.Success); @@ -694,7 +699,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services var result = EntityService.GetId(Guid.Parse("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"), UmbracoObjectTypes.Unknown); Assert.IsTrue(result.Success); - Assert.AreEqual(1052, result.Result); + Assert.AreEqual(_contentType.Id, result.Result); } [Test] @@ -703,7 +708,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services var result = EntityService.GetId(Guid.Parse("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"), UmbracoObjectTypes.DocumentType); Assert.IsTrue(result.Success); - Assert.AreEqual(1052, result.Result); + Assert.AreEqual(_contentType.Id, result.Result); } [Test] @@ -742,6 +747,17 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services private static bool _isSetup = false; private int folderId; + private ContentType _contentType; + private Content _textpage; + private Content _subpage; + private Content _subpage2; + private Content _trashed; + private IMediaType _folderMediaType; + private Media _folder; + private IMediaType _imageMediaType; + private Media _image; + private Media _subfolder; + private Media _subfolder2; public void CreateTestData() { @@ -752,52 +768,52 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services var template = TemplateBuilder.CreateTextPageTemplate(); FileService.SaveTemplate(template); // else, FK violation on contentType! - //Create and Save ContentType "umbTextpage" -> 1052 - var contentType = ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id); - contentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); - ContentTypeService.Save(contentType); + //Create and Save ContentType "umbTextpage" -> _contentType.Id + _contentType = ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id); + _contentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); + ContentTypeService.Save(_contentType); //Create and Save Content "Homepage" based on "umbTextpage" -> 1053 - var textpage = ContentBuilder.CreateSimpleContent(contentType); - textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); - ContentService.Save(textpage, 0); + _textpage = ContentBuilder.CreateSimpleContent(_contentType); + _textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); + ContentService.Save(_textpage, 0); //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 - var subpage = ContentBuilder.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); - subpage.ContentSchedule.Add(DateTime.Now.AddMinutes(-5), null); - ContentService.Save(subpage, 0); + _subpage = ContentBuilder.CreateSimpleContent(_contentType, "Text Page 1", _textpage.Id); + _subpage.ContentSchedule.Add(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(_subpage, 0); //Create and Save Content "Text Page 2" based on "umbTextpage" -> 1055 - var subpage2 = ContentBuilder.CreateSimpleContent(contentType, "Text Page 2", textpage.Id); - ContentService.Save(subpage2, 0); + _subpage2 = ContentBuilder.CreateSimpleContent(_contentType, "Text Page 2", _textpage.Id); + ContentService.Save(_subpage2, 0); //Create and Save Content "Text Page Deleted" based on "umbTextpage" -> 1056 - var trashed = ContentBuilder.CreateSimpleContent(contentType, "Text Page Deleted", -20); - trashed.Trashed = true; - ContentService.Save(trashed, 0); + _trashed = ContentBuilder.CreateSimpleContent(_contentType, "Text Page Deleted", -20); + _trashed.Trashed = true; + ContentService.Save(_trashed, 0); //Create and Save folder-Media -> 1057 - var folderMediaType = MediaTypeService.Get(1031); - var folder = MediaBuilder.CreateMediaFolder(folderMediaType, -1); - MediaService.Save(folder, 0); - folderId = folder.Id; + _folderMediaType = MediaTypeService.Get(1031); + _folder = MediaBuilder.CreateMediaFolder(_folderMediaType, -1); + MediaService.Save(_folder, 0); + folderId = _folder.Id; //Create and Save image-Media -> 1058 - var imageMediaType = MediaTypeService.Get(1032); - var image = MediaBuilder.CreateMediaImage(imageMediaType, folder.Id); - MediaService.Save(image, 0); + _imageMediaType = MediaTypeService.Get(1032); + _image = MediaBuilder.CreateMediaImage(_imageMediaType, _folder.Id); + MediaService.Save(_image, 0); //Create and Save file-Media -> 1059 var fileMediaType = MediaTypeService.Get(1033); - var file = MediaBuilder.CreateMediaFile(fileMediaType, folder.Id); + var file = MediaBuilder.CreateMediaFile(fileMediaType, _folder.Id); MediaService.Save(file, 0); // Create and save sub folder -> 1060 - var subfolder = MediaBuilder.CreateMediaFolder(folderMediaType, folder.Id); - MediaService.Save(subfolder, 0); + _subfolder = MediaBuilder.CreateMediaFolder(_folderMediaType, _folder.Id); + MediaService.Save(_subfolder, 0); // Create and save sub folder -> 1061 - var subfolder2 = MediaBuilder.CreateMediaFolder(folderMediaType, subfolder.Id); - MediaService.Save(subfolder2, 0); + _subfolder2 = MediaBuilder.CreateMediaFolder(_folderMediaType, _subfolder.Id); + MediaService.Save(_subfolder2, 0); } } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs index c3a2866d2e..ca3605217e 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs @@ -23,6 +23,31 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services private IMediaService MediaService => GetRequiredService(); private IMediaTypeService MediaTypeService => GetRequiredService(); + + [Test] + public void Can_Update_Media_Property_Values() + { + IMediaType mediaType = MediaTypeBuilder.CreateSimpleMediaType("test", "Test"); + MediaTypeService.Save(mediaType); + IMedia media = MediaBuilder.CreateSimpleMedia(mediaType, "hello", -1); + media.SetValue("title", "title of mine"); + media.SetValue("bodyText", "hello world"); + MediaService.Save(media); + + // re-get + media = MediaService.GetById(media.Id); + media.SetValue("title", "another title of mine"); // Change a value + media.SetValue("bodyText", null); // Clear a value + media.SetValue("author", "new author"); // Add a value + MediaService.Save(media); + + // re-get + media = MediaService.GetById(media.Id); + Assert.AreEqual("another title of mine", media.GetValue("title")); + Assert.IsNull(media.GetValue("bodyText")); + Assert.AreEqual("new author", media.GetValue("author")); + } + /// /// Used to list out all ambiguous events that will require dispatching with a name /// diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs index d17eead162..a22810ff22 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs @@ -38,22 +38,27 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services } [Test] - public void Can_Update_Member_Property_Value() + public void Can_Update_Member_Property_Values() { IMemberType memberType = MemberTypeBuilder.CreateSimpleMemberType(); MemberTypeService.Save(memberType); IMember member = MemberBuilder.CreateSimpleMember(memberType, "hello", "helloworld@test123.com", "hello", "hello"); member.SetValue("title", "title of mine"); + member.SetValue("bodyText", "hello world"); MemberService.Save(member); // re-get member = MemberService.GetById(member.Id); - member.SetValue("title", "another title of mine"); + member.SetValue("title", "another title of mine"); // Change a value + member.SetValue("bodyText", null); // Clear a value + member.SetValue("author", "new author"); // Add a value MemberService.Save(member); // re-get member = MemberService.GetById(member.Id); Assert.AreEqual("another title of mine", member.GetValue("title")); + Assert.IsNull(member.GetValue("bodyText")); + Assert.AreEqual("new author", member.GetValue("author")); } [Test] diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 59ebaa34e8..040799a09a 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -50,7 +50,7 @@ namespace Umbraco.Tests.UmbracoExamine public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, IScopeProvider scopeProvider, bool publishedValuesOnly) { var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, scopeProvider, publishedValuesOnly); - var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, scopeProvider.SqlContext, contentValueSetBuilder); + var contentIndexDataSource = new ContentIndexPopulator(publishedValuesOnly, null, contentService, scopeProvider.SqlContext, contentValueSetBuilder); return contentIndexDataSource; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 1a8b1341d8..5da7604b7c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -110,18 +110,23 @@ namespace Umbraco.Web.BackOffice.Controllers [UmbracoAuthorize] public ActionResult Frame(int id, string culture) { - var user = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; - - var previewToken = _publishedSnapshotService.EnterPreview(user, id); - - _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, previewToken); + EnterPreview(id); // use a numeric url because content may not be in cache and so .Url would fail var query = culture.IsNullOrWhiteSpace() ? string.Empty : $"?culture={culture}"; return RedirectPermanent($"../../{id}.aspx{query}"); } + public ActionResult EnterPreview(int id) + { + var user = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + var previewToken = _publishedSnapshotService.EnterPreview(user, id); + + _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, previewToken); + + return null; + } public ActionResult End(string redir = null) { var previewToken = _cookieManager.GetPreviewCookieValue(); @@ -130,6 +135,9 @@ namespace Umbraco.Web.BackOffice.Controllers _cookieManager.ExpireCookie(Constants.Web.PreviewCookieName); + // Expire Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. + _cookieManager.ExpireCookie(Constants.Web.AcceptPreviewCookieName); + if (Uri.IsWellFormedUriString(redir, UriKind.Relative) && redir.StartsWith("//") == false && Uri.TryCreate(redir, UriKind.Relative, out var url)) diff --git a/src/Umbraco.Web.BackOffice/Services/IconService.cs b/src/Umbraco.Web.BackOffice/Services/IconService.cs index cfb4c820be..07a52b4446 100644 --- a/src/Umbraco.Web.BackOffice/Services/IconService.cs +++ b/src/Umbraco.Web.BackOffice/Services/IconService.cs @@ -22,11 +22,13 @@ namespace Umbraco.Web.BackOffice.Services _hostingEnvironment = hostingEnvironment; } + /// public IList GetAllIcons() { var icons = new List(); - var iconNames = GetAllIconNames(); + var directory = new DirectoryInfo(_hostingEnvironment.MapPathWebRoot($"{_globalSettings.Value.IconsPath}/")); + var iconNames = directory.GetFiles("*.svg"); iconNames.OrderBy(f => f.Name).ToList().ForEach(iconInfo => { @@ -53,49 +55,29 @@ namespace Umbraco.Web.BackOffice.Services /// Gets an IconModel using values from a FileInfo model /// /// - /// - private IconModel GetIcon(FileSystemInfo fileInfo) + /// + private IconModel GetIcon(FileInfo fileInfo) { return fileInfo == null || string.IsNullOrWhiteSpace(fileInfo.Name) ? null : CreateIconModel(fileInfo.Name.StripFileExtension(), fileInfo.FullName); } - /// - /// Gets an IconModel containing the icon name and SvgString - /// - /// - /// - private IconModel CreateIconModel(string iconName) - { - if (string.IsNullOrWhiteSpace(iconName)) - return null; - - var iconNames = GetAllIconNames(); - var iconPath = iconNames.FirstOrDefault(x => x.Name.InvariantEquals($"{iconName}.svg"))?.FullName; - return iconPath == null - ? null - : CreateIconModel(iconName, iconPath); - } - /// /// Gets an IconModel containing the icon name and SvgString /// /// /// - /// - private static IconModel CreateIconModel(string iconName, string iconPath) + /// + private IconModel CreateIconModel(string iconName, string iconPath) { - if (string.IsNullOrWhiteSpace(iconPath)) - return null; + var sanitizer = new HtmlSanitizer(); + sanitizer.AllowedAttributes.UnionWith(Constants.SvgSanitizer.Attributes); + sanitizer.AllowedCssProperties.UnionWith(Constants.SvgSanitizer.Attributes); + sanitizer.AllowedTags.UnionWith(Constants.SvgSanitizer.Tags); try { - var sanitizer = new HtmlSanitizer(); - sanitizer.AllowedAttributes.UnionWith(Constants.SvgSanitizer.Attributes); - sanitizer.AllowedCssProperties.UnionWith(Constants.SvgSanitizer.Attributes); - sanitizer.AllowedTags.UnionWith(Constants.SvgSanitizer.Tags); - var svgContent = System.IO.File.ReadAllText(iconPath); var sanitizedString = sanitizer.Sanitize(svgContent); @@ -112,27 +94,5 @@ namespace Umbraco.Web.BackOffice.Services return null; } } - - private IEnumerable GetAllIconNames() - { - // TODO: See comment: https://github.com/umbraco/Umbraco-CMS/pull/8884/files#r510564185 - // add icons from plugins - var appPlugins = new DirectoryInfo(_hostingEnvironment.MapPathWebRoot(Constants.SystemDirectories.AppPlugins)); - var pluginIcons = appPlugins.Exists == false - ? new List() - : appPlugins.GetDirectories() - // Find all directories in App_Plugins that are named "Icons" and get a list of SVGs from them - .SelectMany(x => x.GetDirectories("Icons", SearchOption.AllDirectories)) - .SelectMany(x => x.GetFiles("*.svg", SearchOption.TopDirectoryOnly)); - - // add icons from IconsPath if not already added from plugins - var directory = new DirectoryInfo(_hostingEnvironment.MapPathWebRoot($"{_globalSettings.Value.IconsPath}/")); - var iconNames = directory.GetFiles("*.svg") - .Where(x => pluginIcons.Any(i => i.Name == x.Name) == false); - - iconNames = iconNames.Concat(pluginIcons).ToList(); - - return iconNames; - } } } diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index 77deda2f5c..511b6da945 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -30,6 +30,7 @@ module.exports = { // js files for backoffice // processed in the js task js: { + websitepreview: { files: "./src/websitepreview/**/*.js", out: "umbraco.websitepreview.js" }, preview: { files: "./src/preview/**/*.js", out: "umbraco.preview.js" }, installer: { files: "./src/installer/**/*.js", out: "umbraco.installer.js" }, filters: { files: "./src/common/filters/**/*.js", out: "umbraco.filters.js" }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 675378985c..ea8602214d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -164,7 +164,7 @@ function inviteSavePassword() { - if (formHelper.submitForm({ scope: $scope })) { + if (formHelper.submitForm({ scope: $scope, formCtrl: vm.inviteUserPasswordForm })) { vm.invitedUserPasswordModel.buttonState = "busy"; @@ -172,7 +172,7 @@ .then(function (data) { //success - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: vm.inviteUserPasswordForm }); vm.invitedUserPasswordModel.buttonState = "success"; //set the user and set them as logged in vm.invitedUser = data; @@ -181,7 +181,7 @@ vm.inviteStep = 2; }, function (err) { - formHelper.resetForm({ scope: $scope, hasErrors: true }); + formHelper.resetForm({ scope: $scope, hasErrors: true, formCtrl: vm.inviteUserPasswordForm }); formHelper.handleError(err); vm.invitedUserPasswordModel.buttonState = "error"; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js index 12e1144acc..122d430165 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -57,14 +57,32 @@ } } } - function replaceElementTypeBlockListUDIsResolver(obj, propClearingMethod) { - replaceRawBlockListUDIsResolver(obj.value, propClearingMethod); + function removeBlockReferences(obj) { + for (var k in obj) { + if(k === "contentUdi") { + delete obj[k]; + } else if(k === "settingsUdi") { + delete obj[k]; + } else { + // lets crawl through all properties of layout to make sure get captured all `contentUdi` and `settingsUdi` properties. + var propType = typeof obj[k]; + if(propType != null && (propType === "object" || propType === "array")) { + removeBlockReferences(obj[k]) + } + } + } } - clipboardService.registerPastePropertyResolver(replaceElementTypeBlockListUDIsResolver, clipboardService.TYPES.ELEMENT_TYPE); + + function elementTypeBlockResolver(obj, propPasteResolverMethod) { + // we could filter for specific Property Editor Aliases, but as the Block Editor structure can be used by many Property Editor we do not in this code know a good way to detect that this is a Block Editor and will therefor leave it to the value structure to determin this. + rawBlockResolver(obj.value, propPasteResolverMethod); + } + + clipboardService.registerPastePropertyResolver(elementTypeBlockResolver, clipboardService.TYPES.ELEMENT_TYPE); - function replaceRawBlockListUDIsResolver(value, propClearingMethod) { + function rawBlockResolver(value, propPasteResolverMethod) { if (value != null && typeof value === "object") { // we got an object, and it has these three props then we are most likely dealing with a Block Editor. @@ -72,19 +90,19 @@ replaceUdisOfObject(value.layout, value); - // replace UDIs for inner properties of this Block Editors content data. + // run resolvers for inner properties of this Blocks content data. if(value.contentData.length > 0) { value.contentData.forEach((item) => { for (var k in item) { - propClearingMethod(item[k], clipboardService.TYPES.RAW); + propPasteResolverMethod(item[k], clipboardService.TYPES.RAW); } }); } - // replace UDIs for inner properties of this Block Editors settings data. + // run resolvers for inner properties of this Blocks settings data. if(value.settingsData.length > 0) { value.settingsData.forEach((item) => { for (var k in item) { - propClearingMethod(item[k], clipboardService.TYPES.RAW); + propPasteResolverMethod(item[k], clipboardService.TYPES.RAW); } }); } @@ -93,7 +111,29 @@ } } - clipboardService.registerPastePropertyResolver(replaceRawBlockListUDIsResolver, clipboardService.TYPES.RAW); + clipboardService.registerPastePropertyResolver(rawBlockResolver, clipboardService.TYPES.RAW); + + + function provideNewUdisForBlockResolver(block, propPasteResolverMethod) { + + if(block.layout) { + // We do not support layout child blocks currently, these should be stripped out as we only will be copying a single entry. + removeBlockReferences(block.layout); + } + + if(block.data) { + // Make new UDI for content-element + block.data.udi = block.layout.contentUdi = udiService.create("element"); + } + + if(block.settingsData) { + // Make new UDI for settings-element + block.settingsData.udi = block.layout.settingsUdi = udiService.create("element"); + } + + } + + clipboardService.registerPastePropertyResolver(provideNewUdisForBlockResolver, clipboardService.TYPES.BLOCK); }]); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 77ed357c35..8a2b230a27 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -13,7 +13,7 @@ (function () { 'use strict'; - function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService) { + function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService, notificationsService) { /** * Simple mapping from property model content entry to editing model, @@ -773,6 +773,57 @@ return layoutEntry; + }, + /** + * @ngdoc method + * @name createFromBlockData + * @methodOf umbraco.services.blockEditorModelObject + * @description Insert data from raw models + * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration. + */ + createFromBlockData: function (blockData) { + + blockData = clipboardService.parseContentForPaste(blockData, clipboardService.TYPES.BLOCK); + + // As the blockData is a cloned object we can use its layout part for our layout entry. + var layoutEntry = blockData.layout; + if (layoutEntry === null) { + return null; + } + + var blockConfiguration; + + if (blockData.data) { + // Ensure that we support the alias: + blockConfiguration = this.getBlockConfiguration(blockData.data.contentTypeKey); + if(blockConfiguration === null) { + return null; + } + + this.value.contentData.push(blockData.data); + } else { + // We do not have data, this cannot be succesful paste. + return null; + } + + if (blockData.settingsData) { + // Ensure that we support the alias: + if(blockConfiguration.settingsElementTypeKey) { + // If we have settings for this Block Configuration, we need to check that they align, if we dont we do not want to fail. + if(blockConfiguration.settingsElementTypeKey === blockData.settingsData.contentTypeKey) { + this.value.settingsData.push(blockData.settingsData); + } else { + notificationsService.error("Clipboard", "Couldn't paste because settings-data is not compatible."); + return null; + } + } else { + // We do not have settings currently, so lets get rid of the settings part and move on with the paste. + delete layoutEntry.settingUdi; + } + } + + return layoutEntry; + }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 58ed07367e..83fd3d08c2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -10,33 +10,67 @@ * The service has a set way for defining a data-set by a entryType and alias, which later will be used to retrive the posible entries for a paste scenario. * */ -function clipboardService(notificationsService, eventsService, localStorageService, iconHelper) { +function clipboardService($window, notificationsService, eventsService, localStorageService, iconHelper) { const TYPES = {}; TYPES.ELEMENT_TYPE = "elementType"; + TYPES.BLOCK = "block"; TYPES.RAW = "raw"; var clearPropertyResolvers = {}; var pastePropertyResolvers = {}; var clipboardTypeResolvers = {}; - clipboardTypeResolvers[TYPES.ELEMENT_TYPE] = function(data, propMethod) { - for (var t = 0; t < data.variants[0].tabs.length; t++) { - var tab = data.variants[0].tabs[t]; + clipboardTypeResolvers[TYPES.ELEMENT_TYPE] = function(element, propMethod) { + for (var t = 0; t < element.variants[0].tabs.length; t++) { + var tab = element.variants[0].tabs[t]; for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; propMethod(prop, TYPES.ELEMENT_TYPE); } } } + clipboardTypeResolvers[TYPES.BLOCK] = function (block, propMethod) { + + propMethod(block, TYPES.BLOCK); + + if(block.data) { + Object.keys(block.data).forEach( key => { + if(key === 'udi' || key === 'contentTypeKey') { + return; + } + propMethod(block.data[key], TYPES.RAW); + }); + } + + if(block.settingsData) { + Object.keys(block.settingsData).forEach( key => { + if(key === 'udi' || key === 'contentTypeKey') { + return; + } + propMethod(block.settingsData[key], TYPES.RAW); + }); + } + + /* + // Concept for supporting Block that contains other Blocks. + // Missing clarifications: + // How do we ensure that the inner blocks of a block is supported in the new scenario. Not that likely but still relevant, so considerations should be made. + if(block.references) { + // A Block clipboard entry can contain other Block Clipboard Entries, here we will make sure to resolve those identical to the main entry. + for (var r = 0; r < block.references.length; r++) { + clipboardTypeResolvers[TYPES.BLOCK](block.references[r], propMethod); + } + } + */ + } clipboardTypeResolvers[TYPES.RAW] = function(data, propMethod) { for (var p = 0; p < data.length; p++) { propMethod(data[p], TYPES.RAW); } } - var STORAGE_KEY = "umbClipboardService"; var retriveStorage = function() { @@ -64,7 +98,10 @@ function clipboardService(notificationsService, eventsService, localStorageServi var storageString = JSON.stringify(storage); try { + // Check that we can parse the JSON: var storageJSON = JSON.parse(storageString); + + // Store the string: localStorageService.set(STORAGE_KEY, storageString); eventsService.emit("clipboardService.storageUpdate"); @@ -82,11 +119,11 @@ function clipboardService(notificationsService, eventsService, localStorageServi type = type || "raw"; var resolvers = clearPropertyResolvers[type]; - - for (var i=0; i x.properties.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin === true); + return _.some(externalLoginInfo.providers, x => x.properties && x.properties.UmbracoBackOfficeExternalLoginOptions && (x.properties.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin === true)); } else { - return provider.properties.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin; + return provider && provider.properties && provider.properties.UmbracoBackOfficeExternalLoginOptions && (provider.properties.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin === true); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 7962981592..5f0a6a3c00 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -19,7 +19,7 @@ yeah so this is a pain, but we must be super specific in targeting the mandatory property labels, otherwise all properties within a reqired, nested, nested content property will all appear mandatory */ - > ng-form > .control-group > .umb-el-wrap > .control-header label:after { + .umb-property > ng-form > .control-group > .umb-el-wrap > .control-header label:after { content: '*'; color: @red; } diff --git a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js index b312c57503..9a552826dd 100644 --- a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js @@ -5,7 +5,7 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.services']) - .controller("previewController", function ($scope, $window, $location) { + .controller("previewController", function ($scope, $window, $location, $http) { $scope.currentCulture = { iso: '', title: '...', icon: 'icon-loading' } var cultures = []; @@ -111,7 +111,7 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.valueAreLoaded = false; $scope.devices = [ - { name: "fullsize", css: "fullsize", icon: "icon-application-window-alt", title: "Browser" }, + { name: "fullsize", css: "fullsize", icon: "icon-application-window-alt", title: "Fit browser" }, { name: "desktop", css: "desktop shadow", icon: "icon-display", title: "Desktop" }, { name: "laptop - 1366px", css: "laptop shadow", icon: "icon-laptop", title: "Laptop" }, { name: "iPad portrait - 768px", css: "iPad-portrait shadow", icon: "icon-ipad", title: "Tablet portrait" }, @@ -147,17 +147,33 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.windowClickHandler = function () { closeOthers(); } + function windowBlurHandler() { closeOthers(); $scope.$digest(); } + window.addEventListener("blur", windowBlurHandler); - var win = $($window); + function windowVisibilityHandler(e) { - win.on("blur", windowBlurHandler); + var amountOfPreviewSessions = localStorage.getItem('UmbPreviewSessionAmount'); + + // When tab is visible again: + if(document.hidden === false) { + checkPreviewState(); + } + } + document.addEventListener("visibilitychange", windowVisibilityHandler); + + function beforeUnloadHandler(e) { + endPreviewSession(); + } + window.addEventListener("beforeunload", beforeUnloadHandler, false); $scope.$on("$destroy", function () { - win.off("blur", handleBlwindowBlurHandlerur); + window.removeEventListener("blur", windowBlurHandler); + document.removeEventListener("visibilitychange", windowVisibilityHandler); + window.removeEventListener("beforeunload", beforeUnloadHandler); }); @@ -173,6 +189,197 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.pageUrl = "frame?" + query; } } + function getCookie(cname) { + var name = cname + "="; + var ca = document.cookie.split(";"); + for(var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == " ") { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return null; + } + function setCookie(cname, cvalue, exminutes) { + var d = new Date(); + d.setTime(d.getTime() + (exminutes * 60 * 1000)); + document.cookie = cname + "=" + cvalue + ";expires="+d.toUTCString() + ";path=/"; + } + var hasPreviewDialog = false; + function checkPreviewState() { + if (getCookie("UMB_PREVIEW") === null) { + + if(hasPreviewDialog === true) return; + hasPreviewDialog = true; + + // Ask to re-enter preview mode? + + const localizeVarsFallback = { + "returnToPreviewHeadline": "Preview content?", + "returnToPreviewDescription":"You have ended preview mode, do you want to continue previewing this content?", + "returnToPreviewButton":"Preview" + }; + const umbLocalizedVars = Object.assign(localizeVarsFallback, $window.umbLocalizedVars); + + + // This modal is also used in websitepreview.js + var modelStyles = ` + + /* Webfont: LatoLatin-Bold */ + @font-face { + font-family: 'Lato'; + src: url('https://fonts.googleapis.com/css2?family=Lato:wght@700&display=swap'); + font-style: normal; + font-weight: 700; + font-display: swap; + text-rendering: optimizeLegibility; + } + + .umbraco-preview-dialog { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + z-index: 99999999; + top:0; + bottom:0; + left:0; + right:0; + overflow: auto; + background-color: rgba(0,0,0,0.6); + } + + .umbraco-preview-dialog__modal { + background-color: #fff; + border-radius: 6px; + box-shadow: 0 3px 7px rgba(0,0,0,0.3); + margin: auto; + padding: 30px 40px; + width: 100%; + max-width: 540px; + font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; + font-size: 15px; + } + + .umbraco-preview-dialog__headline { + font-weight: 700; + font-size: 22px; + color: #1b264f; + margin-top:10px; + margin-bottom:20px; + } + .umbraco-preview-dialog__question { + margin-bottom:30px; + } + .umbraco-preview-dialog__modal > button { + display: inline-block; + cursor: pointer; + padding: 8px 18px; + text-align: center; + vertical-align: middle; + border-radius: 3px; + border:none; + font-family: inherit; + font-weight: 700; + font-size: 15px; + float:right; + margin-left:10px; + + color: #1b264f; + background-color: #f6f1ef; + } + .umbraco-preview-dialog__modal > button:hover { + color: #2152a3; + background-color: #f6f1ef; + } + .umbraco-preview-dialog__modal > button.umbraco-preview-dialog__continue { + color: #fff; + background-color: #2bc37c; + } + .umbraco-preview-dialog__modal > button.umbraco-preview-dialog__continue:hover { + background-color: #39d38b; + } + `; + + var bodyEl = document.getElementsByTagName("BODY")[0]; + + var fragment = document.createElement("div"); + var shadowRoot = fragment.attachShadow({ mode: 'open' }); + + var style = document.createElement("style"); + style.innerHTML = modelStyles; + shadowRoot.appendChild(style); + + var con = document.createElement("div"); + con.className = "umbraco-preview-dialog"; + shadowRoot.appendChild(con); + + var modal = document.createElement("div"); + modal.className = "umbraco-preview-dialog__modal"; + modal.innerHTML = `
${umbLocalizedVars.returnToPreviewHeadline}
+
${umbLocalizedVars.returnToPreviewDescription}
`; + con.appendChild(modal); + + var continueButton = document.createElement("button"); + continueButton.type = "button"; + continueButton.className = "umbraco-preview-dialog__continue"; + continueButton.innerHTML = umbLocalizedVars.returnToPreviewButton; + continueButton.addEventListener("click", () => { + bodyEl.removeChild(fragment); + reenterPreviewMode(); + hasPreviewDialog = false; + }); + modal.appendChild(continueButton); + + bodyEl.appendChild(fragment); + continueButton.focus(); + + } + } + function reenterPreviewMode() { + //Re-enter Preview Mode + $http({ + method: 'POST', + url: '../preview/enterPreview', + params: { + id: $scope.pageId + } + }) + startPreviewSession(); + } + function getPageURL() { + var culture = $location.search().culture || getParameterByName("culture"); + var relativeUrl = "/" + $scope.pageId; + if (culture) { + relativeUrl += '?culture=' + culture; + } + return relativeUrl; + } + + function startPreviewSession() { + // lets registrer this preview session. + var amountOfPreviewSessions = Math.max(localStorage.getItem('UmbPreviewSessionAmount') || 0, 0); + amountOfPreviewSessions++; + localStorage.setItem('UmbPreviewSessionAmount', amountOfPreviewSessions); + } + function resetPreviewSessions() { + localStorage.setItem('UmbPreviewSessionAmount', 0); + } + function endPreviewSession() { + var amountOfPreviewSessions = localStorage.getItem('UmbPreviewSessionAmount') || 0; + amountOfPreviewSessions--; + localStorage.setItem('UmbPreviewSessionAmount', amountOfPreviewSessions); + + if(amountOfPreviewSessions <= 0) { + // We are good to end preview mode. + navigator.sendBeacon("../preview/end"); + } + } + startPreviewSession(); + /*****************************************************************************/ /* Preview devices */ /*****************************************************************************/ @@ -182,21 +389,26 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.previewDevice = device; }; + /*****************************************************************************/ + /* Open website in preview mode */ + /*****************************************************************************/ + + $scope.openInBrowser = function () { + setCookie("UMB-WEBSITE-PREVIEW-ACCEPT", "true", 5); + window.open(getPageURL(), "_blank"); + }; + /*****************************************************************************/ /* Exit Preview */ /*****************************************************************************/ $scope.exitPreview = function () { - - var culture = $location.search().culture || getParameterByName("culture"); - var relativeUrl = "/" + $scope.pageId; - if (culture) { - relativeUrl += '?culture=' + culture; - } - window.top.location.href = "../preview/end?redir=" + encodeURIComponent(relativeUrl); + resetPreviewSessions(); + window.top.location.href = "../preview/end?redir=" + encodeURIComponent(getPageURL()); }; $scope.onFrameLoaded = function (iframe) { + $scope.frameLoaded = true; configureSignalR(iframe); @@ -250,8 +462,8 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi var vm = this; vm.$postLink = function () { - var resultFrame = $element.find("#resultFrame"); - resultFrame.on("load", iframeReady); + var resultFrame = $element.find("#resultFrame").get(0); + resultFrame.addEventListener("load", iframeReady); }; function iframeReady() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html index 405ca1d470..1e4cc70b7f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -1,5 +1,5 @@
-