Introduced sign providers for trees and implemented one for documents with schedule pending (#19806)

* Create sign provider collection and call registered providers on rendering a page of tree item view models.
Re-work tree controller constructors to provide registered providers as a collection.

* Stub implementation of sign provider for documents with a scheduled publish pending.

* Complete implementation of tree sign for pending scheduled publish.

* Added integration test for new method on IContentService.

* Added unit test for HasScheduleSignProvider.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Tidied usings and clarified method header comments.

* Adding a fixed prefix to all future signs, and removing the provider property

* Adding a sign for protected tree documents.

* Adding IsProtectedSignProviderTest.cs & correcting HasScheduleSignProviderTests.cs to no longer assert the provider

* Fixing minor things in accordance with CR

* Adding collection items compatibility

* Introduced IHasSigns interface to provide more re-use across trees and collections.
Fixed updates to base content controllers (no need to introduce a new type variable).
Removed passing entities for populating tree signs (we aren't using it, so simplifies things).

* Refactoring a bit to make existing code less duplicated and fixing some constructor obsoletion

* Introducing a has pending changes sign.

* Applying changes based on CR

* Introducing tests for HasPendingChangesSignProvider.cs and stopped the use of contentService

* Introducing tests for HasPendingChangesSignProvider.cs and slight logic change

* Introduced HasCollectionSignProvider.cs and tests.

* Introducing collection signs to Media Tree & Media Collection items

* Introducing Plain Items and tests. Refactoring tests as well

* Introduced alternative CanProvideSigns() implementation on IsProtectedSignProvider.cs

* Slight refactoring to reduce bloating.

* Adding [ActivatorUtilitiesConstructor] since it threw an error otherwise

* Minor cleanup.

* Updated OpenApi.json.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: NillasKA <kramernicklas@gmail.com>
This commit is contained in:
Andy Butland
2025-08-20 10:32:23 +01:00
committed by GitHub
parent 467e55c5aa
commit cebfb21eec
88 changed files with 2253 additions and 130 deletions

View File

@@ -687,6 +687,25 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent
Assert.That(contents.Count(), Is.EqualTo(1));
}
[Test]
public void Can_Get_Scheduled_Content_Keys()
{
// Arrange
var root = ContentService.GetById(Textpage.Id);
ContentService.Publish(root!, root!.AvailableCultures.ToArray());
var content = ContentService.GetById(Subpage.Id);
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddDays(1), null);
ContentService.PersistContentSchedule(content!, contentSchedule);
ContentService.Publish(content, content.AvailableCultures.ToArray());
// Act
var keys = ContentService.GetScheduledContentKeys([Textpage.Key, Subpage.Key, Subpage2.Key]).ToList();
// Assert
Assert.AreEqual(1, keys.Count);
Assert.AreEqual(Subpage.Key, keys.First());
}
[Test]
public void Can_Get_Content_For_Release()
{

View File

@@ -0,0 +1,196 @@
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Services.Signs;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Api.Management.ViewModels.Media.Collection;
using Umbraco.Cms.Api.Management.ViewModels.Media.Item;
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs;
[TestFixture]
internal class HasCollectionSignProviderTests
{
[Test]
public void HasCollectionSignProvider_Can_Provide_Document_Tree_Signs()
{
var sut = new HasCollectionSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentTreeItemResponseModel>());
}
[Test]
public void HasCollectionSignProvider_Can_Provide_Document_Collection_Signs()
{
var sut = new HasCollectionSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentCollectionResponseModel>());
}
[Test]
public void HasCollectionSignProvider_Can_Provide_Document_Item_Signs()
{
var sut = new HasCollectionSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentItemResponseModel>());
}
[Test]
public void HasCollectionSignProvider_Can_Provide_Media_Tree_Signs()
{
var sut = new HasCollectionSignProvider();
Assert.IsTrue(sut.CanProvideSigns<MediaTreeItemResponseModel>());
}
[Test]
public void HasCollectionSignProvider_Can_Provide_Media_Collection_Signs()
{
var sut = new HasCollectionSignProvider();
Assert.IsTrue(sut.CanProvideSigns<MediaCollectionResponseModel>());
}
[Test]
public void HasCollectionSignProvider_Can_Provide_Media_Item_Signs()
{
var sut = new HasCollectionSignProvider();
Assert.IsTrue(sut.CanProvideSigns<MediaItemResponseModel>());
}
[Test]
public async Task HasCollectionSignProvider_Should_Populate_Document_Tree_Signs()
{
var sut = new HasCollectionSignProvider();
var viewModels = new List<DocumentTreeItemResponseModel>
{
new()
{
Id = Guid.NewGuid(), DocumentType = new DocumentTypeReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) },
},
new() { Id = Guid.NewGuid() },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 1);
Assert.AreEqual(viewModels[1].Signs.Count(), 0);
var signModel = viewModels[0].Signs.First();
Assert.AreEqual("Umb.HasCollection", signModel.Alias);
}
[Test]
public async Task HasCollectionSignProvider_Should_Populate_Document_Collection_Signs()
{
var sut = new HasCollectionSignProvider();
var viewModels = new List<DocumentCollectionResponseModel>
{
new()
{
Id = Guid.NewGuid(), DocumentType = new DocumentTypeCollectionReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) },
},
new() { Id = Guid.NewGuid() },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 1);
Assert.AreEqual(viewModels[1].Signs.Count(), 0);
var signModel = viewModels[0].Signs.First();
Assert.AreEqual("Umb.HasCollection", signModel.Alias);
}
[Test]
public async Task HasCollectionSignProvider_Should_Populate_Document_Item_Signs()
{
var sut = new HasCollectionSignProvider();
var viewModels = new List<DocumentItemResponseModel>
{
new()
{
Id = Guid.NewGuid(), DocumentType = new DocumentTypeReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) },
},
new() { Id = Guid.NewGuid() },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 1);
Assert.AreEqual(viewModels[1].Signs.Count(), 0);
var signModel = viewModels[0].Signs.First();
Assert.AreEqual("Umb.HasCollection", signModel.Alias);
}
[Test]
public async Task HasCollectionSignProvider_Should_Populate_Media_Tree_Signs()
{
var sut = new HasCollectionSignProvider();
var viewModels = new List<MediaTreeItemResponseModel>
{
new()
{
Id = Guid.NewGuid(), MediaType = new MediaTypeReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) },
},
new() { Id = Guid.NewGuid() },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 1);
Assert.AreEqual(viewModels[1].Signs.Count(), 0);
var signModel = viewModels[0].Signs.First();
Assert.AreEqual("Umb.HasCollection", signModel.Alias);
}
[Test]
public async Task HasCollectionSignProvider_Should_Populate_Media_Collection_Signs()
{
var sut = new HasCollectionSignProvider();
var viewModels = new List<MediaCollectionResponseModel>
{
new()
{
Id = Guid.NewGuid(), MediaType = new MediaTypeCollectionReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) },
},
new() { Id = Guid.NewGuid() },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 1);
Assert.AreEqual(viewModels[1].Signs.Count(), 0);
var signModel = viewModels[0].Signs.First();
Assert.AreEqual("Umb.HasCollection", signModel.Alias);
}
[Test]
public async Task HasCollectionSignProvider_Should_Populate_Media_Item_Signs()
{
var sut = new HasCollectionSignProvider();
var viewModels = new List<MediaItemResponseModel>
{
new()
{
Id = Guid.NewGuid(), MediaType = new MediaTypeReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) },
},
new() { Id = Guid.NewGuid() },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 1);
Assert.AreEqual(viewModels[1].Signs.Count(), 0);
var signModel = viewModels[0].Signs.First();
Assert.AreEqual("Umb.HasCollection", signModel.Alias);
}
}

View File

@@ -0,0 +1,126 @@
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Services.Signs;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs;
[TestFixture]
internal class HasPendingChangesSignProviderTests
{
[Test]
public void HasPendingChangesSignProvider_Can_Provide_Document_Tree_Signs()
{
var sut = new HasPendingChangesSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentTreeItemResponseModel>());
}
[Test]
public void HasPendingChangesSignProvider_Can_Provide_Document_Collection_Signs()
{
var sut = new HasPendingChangesSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentCollectionResponseModel>());
}
[Test]
public void HasPendingChangesSignProvider_Can_Provide_Document_Item_Signs()
{
var sut = new HasPendingChangesSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentItemResponseModel>());
}
[Test]
public async Task HasPendingChangesSignProvider_Should_Populate_Document_Tree_Signs()
{
var sut = new HasPendingChangesSignProvider();
var viewModels = new List<DocumentTreeItemResponseModel>
{
new() { Id = Guid.NewGuid() },
new()
{
Id = Guid.NewGuid(), Variants =
[
new()
{
State = DocumentVariantState.PublishedPendingChanges,
Culture = null,
Name = "Test",
},
],
},
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 0);
Assert.AreEqual(viewModels[1].Signs.Count(), 1);
var signModel = viewModels[1].Signs.First();
Assert.AreEqual("Umb.PendingChanges", signModel.Alias);
}
[Test]
public async Task HasPendingChangesSignProvider_Should_Populate_Document_Collection_Signs()
{
var sut = new HasPendingChangesSignProvider();
var viewModels = new List<DocumentCollectionResponseModel>
{
new() { Id = Guid.NewGuid() },
new()
{
Id = Guid.NewGuid(), Variants =
[
new()
{
State = DocumentVariantState.PublishedPendingChanges,
Culture = null,
Name = "Test",
},
],
},
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 0);
Assert.AreEqual(viewModels[1].Signs.Count(), 1);
var signModel = viewModels[1].Signs.First();
Assert.AreEqual("Umb.PendingChanges", signModel.Alias);
}
[Test]
public async Task HasPendingChangesSignProvider_Should_Populate_Document_Item_Signs()
{
var sut = new HasPendingChangesSignProvider();
var viewModels = new List<DocumentItemResponseModel>
{
new() { Id = Guid.NewGuid() },
new()
{
Id = Guid.NewGuid(), Variants =
[
new()
{
State = DocumentVariantState.PublishedPendingChanges,
Culture = null,
Name = "Test",
},
],
},
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 0);
Assert.AreEqual(viewModels[1].Signs.Count(), 1);
var signModel = viewModels[1].Signs.First();
Assert.AreEqual("Umb.PendingChanges", signModel.Alias);
}
}

View File

@@ -0,0 +1,125 @@
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Services.Signs;
using Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs;
[TestFixture]
internal class HasScheduleSignProviderTests
{
[Test]
public void HasScheduleSignProvider_Can_Provide_Document_Tree_Signs()
{
var contentServiceMock = new Mock<IContentService>();
var sut = new HasScheduleSignProvider(contentServiceMock.Object);
Assert.IsTrue(sut.CanProvideSigns<DocumentTreeItemResponseModel>());
}
[Test]
public void HasScheduleSignProvider_Can_Provide_Document_Collection_Signs()
{
var contentServiceMock = new Mock<IContentService>();
var sut = new HasScheduleSignProvider(contentServiceMock.Object);
Assert.IsTrue(sut.CanProvideSigns<DocumentCollectionResponseModel>());
}
[Test]
public void HasScheduleSignProvider_Can_Provide_Document_Item_Signs()
{
var contentServiceMock = new Mock<IContentService>();
var sut = new HasScheduleSignProvider(contentServiceMock.Object);
Assert.IsTrue(sut.CanProvideSigns<DocumentItemResponseModel>());
}
[Test]
public async Task HasScheduleSignProvider_Should_Populate_Document_Tree_Signs()
{
var entities = new List<EntitySlim>
{
new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" },
};
var contentServiceMock = new Mock<IContentService>();
contentServiceMock
.Setup(x => x.GetScheduledContentKeys(It.IsAny<IEnumerable<Guid>>()))
.Returns([entities[1].Key]);
var sut = new HasScheduleSignProvider(contentServiceMock.Object);
var viewModels = new List<DocumentTreeItemResponseModel>
{
new() { Id = entities[0].Key }, new() { Id = entities[1].Key },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 0);
Assert.AreEqual(viewModels[1].Signs.Count(), 1);
var signModel = viewModels[1].Signs.First();
Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias);
}
[Test]
public async Task HasScheduleSignProvider_Should_Populate_Document_Collection_Signs()
{
var entities = new List<EntitySlim>
{
new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" },
};
var contentServiceMock = new Mock<IContentService>();
contentServiceMock
.Setup(x => x.GetScheduledContentKeys(It.IsAny<IEnumerable<Guid>>()))
.Returns([entities[1].Key]);
var sut = new HasScheduleSignProvider(contentServiceMock.Object);
var viewModels = new List<DocumentCollectionResponseModel>
{
new() { Id = entities[0].Key }, new() { Id = entities[1].Key },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 0);
Assert.AreEqual(viewModels[1].Signs.Count(), 1);
var signModel = viewModels[1].Signs.First();
Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias);
}
[Test]
public async Task HasScheduleSignProvider_Should_Populate_Document_Item_Signs()
{
var entities = new List<EntitySlim>
{
new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" },
};
var contentServiceMock = new Mock<IContentService>();
contentServiceMock
.Setup(x => x.GetScheduledContentKeys(It.IsAny<IEnumerable<Guid>>()))
.Returns([entities[1].Key]);
var sut = new HasScheduleSignProvider(contentServiceMock.Object);
var viewModels = new List<DocumentItemResponseModel>
{
new() { Id = entities[0].Key }, new() { Id = entities[1].Key },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 0);
Assert.AreEqual(viewModels[1].Signs.Count(), 1);
var signModel = viewModels[1].Signs.First();
Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias);
}
}

View File

@@ -0,0 +1,92 @@
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Services.Signs;
using Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs;
[TestFixture]
internal class IsProtectedSignProviderTests
{
[Test]
public void IsProtectedSignProvider_Can_Provide_Tree_Signs()
{
var sut = new IsProtectedSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentTreeItemResponseModel>());
}
[Test]
public void IsProtectedSignProvider_Can_Provide_Collection_Signs()
{
var sut = new IsProtectedSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentCollectionResponseModel>());
}
[Test]
public void IsProtectedSignProvider_Can_Provide_Plain_Signs()
{
var sut = new IsProtectedSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentItemResponseModel>());
}
[Test]
public async Task IsProtectedSignProvider_Should_Populate_Tree_Signs()
{
var sut = new IsProtectedSignProvider();
var viewModels = new List<DocumentTreeItemResponseModel>
{
new(),
new() { IsProtected = true },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 0);
Assert.AreEqual(viewModels[1].Signs.Count(), 1);
var signModel = viewModels[1].Signs.First();
Assert.AreEqual("Umb.IsProtected", signModel.Alias);
}
[Test]
public async Task IsProtectedSignProvider_Should_Populate_Collection_Signs()
{
var sut = new IsProtectedSignProvider();
var viewModels = new List<DocumentCollectionResponseModel>
{
new(),
new() { IsProtected = true },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 0);
Assert.AreEqual(viewModels[1].Signs.Count(), 1);
var signModel = viewModels[1].Signs.First();
Assert.AreEqual("Umb.IsProtected", signModel.Alias);
}
[Test]
public async Task IsProtectedSignProvider_Should_Populate_Plain_Signs()
{
var sut = new IsProtectedSignProvider();
var viewModels = new List<DocumentItemResponseModel>
{
new(),
new() { IsProtected = true },
};
await sut.PopulateSignsAsync(viewModels);
Assert.AreEqual(viewModels[0].Signs.Count(), 0);
Assert.AreEqual(viewModels[1].Signs.Count(), 1);
var signModel = viewModels[1].Signs.First();
Assert.AreEqual("Umb.IsProtected", signModel.Alias);
}
}