diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs
index d18fb4b091..516192b905 100644
--- a/src/Umbraco.Core/ContentVariationExtensions.cs
+++ b/src/Umbraco.Core/ContentVariationExtensions.cs
@@ -115,7 +115,7 @@ namespace Umbraco.Core
///
/// Determines whether a variation varies by culture and segment.
///
- public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) > 0;
+ public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment;
///
/// Validates that a combination of culture and segment is valid for the variation.
diff --git a/src/Umbraco.Core/Models/ApplicationTree.cs b/src/Umbraco.Core/Models/ApplicationTree.cs
index 8b0bbc29c4..ccdebea724 100644
--- a/src/Umbraco.Core/Models/ApplicationTree.cs
+++ b/src/Umbraco.Core/Models/ApplicationTree.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
+using Umbraco.Core.Services;
namespace Umbraco.Core.Models
{
@@ -35,6 +36,7 @@ namespace Umbraco.Core.Models
IconClosed = iconClosed;
IconOpened = iconOpened;
Type = type;
+
}
///
@@ -85,6 +87,33 @@ namespace Umbraco.Core.Models
/// The type.
public string Type { get; set; }
+ ///
+ /// Returns the localized root node display name
+ ///
+ ///
+ ///
+ public string GetRootNodeDisplayName(ILocalizedTextService textService)
+ {
+ var label = $"[{Alias}]";
+
+ // try to look up a the localized tree header matching the tree alias
+ var localizedLabel = textService.Localize("treeHeaders/" + Alias);
+
+ // if the localizedLabel returns [alias] then return the title attribute from the trees.config file, if it's defined
+ if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase))
+ {
+ if (string.IsNullOrEmpty(Title) == false)
+ label = Title;
+ }
+ else
+ {
+ // the localizedLabel translated into something that's not just [alias], so use the translation
+ label = localizedLabel;
+ }
+
+ return label;
+ }
+
private Type _runtimeType;
///
diff --git a/src/Umbraco.Core/Models/ContentCultureInfos.cs b/src/Umbraco.Core/Models/ContentCultureInfos.cs
index bcf1dbb1b1..f51e3a275a 100644
--- a/src/Umbraco.Core/Models/ContentCultureInfos.cs
+++ b/src/Umbraco.Core/Models/ContentCultureInfos.cs
@@ -28,11 +28,11 @@ namespace Umbraco.Core.Models
/// Initializes a new instance of the class.
///
/// Used for cloning, without change tracking.
- private ContentCultureInfos(string culture, string name, DateTime date)
- : this(culture)
+ internal ContentCultureInfos(ContentCultureInfos other)
+ : this(other.Culture)
{
- _name = name;
- _date = date;
+ _name = other.Name;
+ _date = other.Date;
}
///
@@ -61,7 +61,7 @@ namespace Umbraco.Core.Models
///
public object DeepClone()
{
- return new ContentCultureInfos(Culture, Name, Date);
+ return new ContentCultureInfos(this);
}
///
diff --git a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs
index 5238e65631..82b0ba6475 100644
--- a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs
+++ b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs
@@ -24,8 +24,12 @@ namespace Umbraco.Core.Models
public ContentCultureInfosCollection(IEnumerable items)
: base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase)
{
+ // make sure to add *copies* and not the original items,
+ // as items can be modified by AddOrUpdate, and therefore
+ // the new collection would be impacted by changes made
+ // to the old collection
foreach (var item in items)
- Add(item);
+ Add(new ContentCultureInfos(item));
}
///
diff --git a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs
index 0d2f817660..8af48bb881 100644
--- a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs
+++ b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs
@@ -63,20 +63,5 @@ namespace Umbraco.Core.Models
aliases = a;
return hasAnyPropertyVariationChanged;
}
-
- ///
- /// Returns the list of content types the composition is used in
- ///
- ///
- ///
- ///
- internal static IEnumerable GetWhereCompositionIsUsedInContentTypes(this IContentTypeComposition source,
- IContentTypeComposition[] allContentTypes)
- {
- var sourceId = source != null ? source.Id : 0;
-
- // find which content types are using this composition
- return allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray();
- }
}
}
diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs
index cf43b661c7..08b9f74802 100644
--- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs
+++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs
@@ -114,6 +114,27 @@ namespace Umbraco.Core.Models
}
}
+ ///
+ /// Gets the property types obtained via composition.
+ ///
+ ///
+ /// Gets them raw, ie with their original variation.
+ ///
+ [IgnoreDataMember]
+ internal IEnumerable RawComposedPropertyTypes => GetRawComposedPropertyTypes();
+
+ private IEnumerable GetRawComposedPropertyTypes(bool start = true)
+ {
+ var propertyTypes = ContentTypeComposition
+ .Cast()
+ .SelectMany(x => start ? x.GetRawComposedPropertyTypes(false) : x.CompositionPropertyTypes);
+
+ if (!start)
+ propertyTypes = propertyTypes.Union(PropertyTypes);
+
+ return propertyTypes;
+ }
+
///
/// Adds a content type to the composition.
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs
index 3bb1ac38ca..cc9b86c56b 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs
@@ -11,13 +11,6 @@ namespace Umbraco.Core.Persistence.Repositories
TItem Get(string alias);
IEnumerable> Move(TItem moving, EntityContainer container);
- ///
- /// Returns the content types that are direct compositions of the content type
- ///
- /// The content type id
- ///
- IEnumerable GetTypesDirectlyComposedOf(int id);
-
///
/// Derives a unique alias from an existing alias.
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs
index aa61383f85..4bec3160a7 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs
@@ -67,7 +67,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, IsPublishing, this, _templateRepository);
}
-
protected override IEnumerable PerformGetAll(params Guid[] ids)
{
// use the underlying GetAll which will force cache all content types
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
index bea6eb9bce..3184c69dfe 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
@@ -119,7 +119,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected void PersistNewBaseContentType(IContentTypeComposition entity)
{
-
var dto = ContentTypeFactory.BuildContentTypeDto(entity);
//Cannot add a duplicate content type type
@@ -234,7 +233,6 @@ AND umbracoNode.nodeObjectType = @objectType",
protected void PersistUpdatedBaseContentType(IContentTypeComposition entity)
{
-
var dto = ContentTypeFactory.BuildContentTypeDto(entity);
// ensure the alias is not used already
@@ -270,8 +268,8 @@ AND umbracoNode.id <> @id",
// 1. Find content based on the current ContentType: entity.Id
// 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key)
// 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one
- var compositionBase = entity as ContentTypeCompositionBase;
- if (compositionBase != null && compositionBase.RemovedContentTypeKeyTracker != null &&
+ if (entity is ContentTypeCompositionBase compositionBase &&
+ compositionBase.RemovedContentTypeKeyTracker != null &&
compositionBase.RemovedContentTypeKeyTracker.Any())
{
//TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating?
@@ -314,7 +312,7 @@ AND umbracoNode.id <> @id",
}
}
- // delete the allowed content type entries before re-inserting the collectino of allowed content types
+ // delete the allowed content type entries before re-inserting the collection of allowed content types
Database.Delete("WHERE Id = @Id", new { entity.Id });
foreach (var allowedContentType in entity.AllowedContentTypes)
{
@@ -409,40 +407,34 @@ AND umbracoNode.id <> @id",
}
//check if the content type variation has been changed
- var ctVariationChanging = entity.IsPropertyDirty("Variations");
- if (ctVariationChanging)
+ var contentTypeVariationDirty = entity.IsPropertyDirty("Variations");
+ var oldContentTypeVariation = (ContentVariation) dtoPk.Variations;
+ var newContentTypeVariation = entity.Variations;
+ var contentTypeVariationChanging = contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation;
+ if (contentTypeVariationChanging)
{
- //we've already looked up the previous version of the content type so we know it's previous variation state
- MoveVariantData(entity, (ContentVariation)dtoPk.Variations, entity.Variations);
+ MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation);
Clear301Redirects(entity);
ClearScheduledPublishing(entity);
- }
+ }
- //track any content type/property types that are changing variation which will require content updates
- var propertyTypeVariationChanges = new Dictionary();
+ // collect property types that have a dirty variation
+ List propertyTypeVariationDirty = null;
- // insert or update properties
- // all of them, no-group and in-groups
+ // note: this only deals with *local* property types, we're dealing w/compositions later below
foreach (var propertyType in entity.PropertyTypes)
{
- //if the content type variation isn't changing track if any property type is changing
- if (!ctVariationChanging)
+ if (contentTypeVariationChanging)
{
- if (propertyType.IsPropertyDirty("Variations"))
+ // content type is changing
+ switch (newContentTypeVariation)
{
- propertyTypeVariationChanges[propertyType.Id] = propertyType.Variations;
- }
- }
- else
- {
- switch(entity.Variations)
- {
- case ContentVariation.Nothing:
- //if the content type is changing to Nothing, then all property type's must change to nothing
+ case ContentVariation.Nothing: // changing to Nothing
+ // all property types must change to Nothing
propertyType.Variations = ContentVariation.Nothing;
break;
- case ContentVariation.Culture:
- //we don't need to modify the property type in this case
+ case ContentVariation.Culture: // changing to Culture
+ // all property types can remain Nothing
break;
case ContentVariation.CultureAndSegment:
case ContentVariation.Segment:
@@ -451,15 +443,65 @@ AND umbracoNode.id <> @id",
}
}
- var groupId = propertyType.PropertyGroupId?.Value ?? default(int);
+ // then, track each property individually
+ if (propertyType.IsPropertyDirty("Variations"))
+ {
+ // allocate the list only when needed
+ if (propertyTypeVariationDirty == null)
+ propertyTypeVariationDirty = new List();
+
+ propertyTypeVariationDirty.Add(propertyType);
+ }
+ }
+
+ // figure out dirty property types that have actually changed
+ // before we insert or update properties, so we can read the old variations
+ var propertyTypeVariationChanges = propertyTypeVariationDirty != null
+ ? GetPropertyVariationChanges(propertyTypeVariationDirty)
+ : null;
+
+ // deal with composition property types
+ // add changes for property types obtained via composition, which change due
+ // to this content type variations change
+ if (contentTypeVariationChanging)
+ {
+ // must use RawComposedPropertyTypes here: only those types that are obtained
+ // via composition, with their original variations (ie not filtered by this
+ // content type variations - we need this true value to make decisions.
+
+ foreach (var propertyType in ((ContentTypeCompositionBase) entity).RawComposedPropertyTypes)
+ {
+ if (propertyType.VariesBySegment() || newContentTypeVariation.VariesBySegment())
+ throw new NotSupportedException(); // TODO: support this
+
+ if (propertyType.Variations == ContentVariation.Culture)
+ {
+ if (propertyTypeVariationChanges == null)
+ propertyTypeVariationChanges = new Dictionary();
+
+ // if content type moves to Culture, property type becomes Culture here again
+ // if content type moves to Nothing, property type becomes Nothing here
+ if (newContentTypeVariation == ContentVariation.Culture)
+ propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Nothing, ContentVariation.Culture);
+ else if (newContentTypeVariation == ContentVariation.Nothing)
+ propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Culture, ContentVariation.Nothing);
+ }
+ }
+ }
+
+ // insert or update properties
+ // all of them, no-group and in-groups
+ foreach (var propertyType in entity.PropertyTypes)
+ {
// if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias
- if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default(int))
+ if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default)
AssignDataTypeFromPropertyEditor(propertyType);
// validate the alias
ValidateAlias(propertyType);
// insert or update property
+ var groupId = propertyType.PropertyGroupId?.Value ?? default;
var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id);
var typeId = propertyType.HasIdentity
? Database.Update(propertyTypeDto)
@@ -470,31 +512,22 @@ AND umbracoNode.id <> @id",
typeId = propertyType.Id;
// not an orphan anymore
- if (orphanPropertyTypeIds != null)
- orphanPropertyTypeIds.Remove(typeId);
+ orphanPropertyTypeIds?.Remove(typeId);
}
- //check if any property types were changing variation
- if (propertyTypeVariationChanges.Count > 0)
- {
- var changes = new Dictionary();
+ // must restrict property data changes to impacted content types - if changing a composing
+ // type, some composed types (those that do not vary) are not impacted and should be left
+ // unchanged
+ //
+ // getting 'all' from the cache policy is prone to race conditions - fast but dangerous
+ //var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll);
+ var all = PerformGetAll();
- //now get the current property type variations for the changed ones so that we know which variation they
- //are going from and to
- var from = Database.Dictionary(Sql()
- .Select(x => x.Id, x => x.Variations)
- .From()
- .WhereIn(x => x.Id, propertyTypeVariationChanges.Keys));
-
- foreach (var f in from)
- {
- changes[f.Key] = (propertyTypeVariationChanges[f.Key], (ContentVariation)f.Value);
- }
-
- //perform the move
- MoveVariantData(changes);
- }
+ var impacted = GetImpactedContentTypes(entity, all);
+ // if some property types have actually changed, move their variant data
+ if (propertyTypeVariationChanges != null)
+ MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted);
// deal with orphan properties: those that were in a deleted tab,
// and have not been re-mapped to another tab or to 'generic properties'
@@ -503,6 +536,77 @@ AND umbracoNode.id <> @id",
DeletePropertyType(entity.Id, id);
}
+ private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable all)
+ {
+ var impact = new List();
+ var set = new List { contentType };
+
+ var tree = new Dictionary>();
+ foreach (var x in all)
+ foreach (var y in x.ContentTypeComposition)
+ {
+ if (!tree.TryGetValue(y.Id, out var list))
+ list = tree[y.Id] = new List();
+ list.Add(x);
+ }
+
+ var nset = new List();
+ do
+ {
+ impact.AddRange(set);
+
+ foreach (var x in set)
+ {
+ if (!tree.TryGetValue(x.Id, out var list)) continue;
+ nset.AddRange(list.Where(y => y.VariesByCulture()));
+ }
+
+ set = nset;
+ nset = new List();
+ } while (set.Count > 0);
+
+ return impact;
+ }
+
+ // gets property types that have actually changed, and the corresponding changes
+ // returns null if no property type has actually changed
+ private Dictionary GetPropertyVariationChanges(IEnumerable propertyTypes)
+ {
+ var propertyTypesL = propertyTypes.ToList();
+
+ // select the current variations (before the change) from database
+ var selectCurrentVariations = Sql()
+ .Select(x => x.Id, x => x.Variations)
+ .From()
+ .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id));
+
+ var oldVariations = Database.Dictionary(selectCurrentVariations);
+
+ // build a dictionary of actual changes
+ Dictionary changes = null;
+
+ foreach (var propertyType in propertyTypesL)
+ {
+ // new property type, ignore
+ if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB))
+ continue;
+ var oldVariation = (ContentVariation) oldVariationB; // NPoco cannot fetch directly
+
+ // only those property types that *actually* changed
+ var newVariation = propertyType.Variations;
+ if (oldVariation == newVariation)
+ continue;
+
+ // allocate the dictionary only when needed
+ if (changes == null)
+ changes = new Dictionary();
+
+ changes[propertyType.Id] = (oldVariation, newVariation);
+ }
+
+ return changes;
+ }
+
///
/// Clear any redirects associated with content for a content type
///
@@ -529,28 +633,39 @@ AND umbracoNode.id <> @id",
}
///
- /// Moves variant data for property type changes
+ /// Gets the default language identifier.
///
- ///
- private void MoveVariantData(IDictionary propertyTypeChanges)
+ private int GetDefaultLanguageId()
{
- var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault));
+ var selectDefaultLanguageId = Sql()
+ .Select(x => x.Id)
+ .From()
+ .Where(x => x.IsDefault);
+
+ return Database.First(selectDefaultLanguageId);
+ }
+
+ ///
+ /// Moves variant data for property type variation changes.
+ ///
+ private void MovePropertyTypeVariantData(IDictionary propertyTypeChanges, IEnumerable impacted)
+ {
+ var defaultLanguageId = GetDefaultLanguageId();
+ var impactedL = impacted.Select(x => x.Id).ToList();
//Group by the "To" variation so we can bulk update in the correct batches
- foreach(var g in propertyTypeChanges.GroupBy(x => x.Value.Item2))
+ foreach(var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation))
{
- var propertyTypeIds = g.Select(s => s.Key).ToList();
+ var propertyTypeIds = grouping.Select(x => x.Key).ToList();
+ var toVariation = grouping.Key;
- //the ContentVariation that the data is moving "To"
- var toVariantType = g.Key;
-
- switch(toVariantType)
+ switch (toVariation)
{
case ContentVariation.Culture:
- MovePropertyDataToVariantCulture(defaultLangId, propertyTypeIds: propertyTypeIds);
+ CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL);
break;
case ContentVariation.Nothing:
- MovePropertyDataToVariantNothing(defaultLangId, propertyTypeIds: propertyTypeIds);
+ CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL);
break;
case ContentVariation.CultureAndSegment:
case ContentVariation.Segment:
@@ -561,24 +676,17 @@ AND umbracoNode.id <> @id",
}
///
- /// Moves variant data for a content type variation change
+ /// Moves variant data for a content type variation change.
///
- ///
- ///
- ///
- private void MoveVariantData(IContentTypeComposition contentType, ContentVariation from, ContentVariation to)
+ private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, ContentVariation toVariation)
{
- var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault));
+ var defaultLanguageId = GetDefaultLanguageId();
- var sqlPropertyTypeIds = Sql().Select(x => x.Id).From().Where(x => x.ContentTypeId == contentType.Id);
- switch (to)
+ switch (toVariation)
{
case ContentVariation.Culture:
- //move the property data
- MovePropertyDataToVariantCulture(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds);
-
- //now we need to move the names
+ //move the names
//first clear out any existing names that might already exists under the default lang
//there's 2x tables to update
@@ -588,10 +696,11 @@ AND umbracoNode.id <> @id",
.InnerJoin().On(x => x.Id, x => x.VersionId)
.InnerJoin().On(x => x.NodeId, x => x.NodeId)
.Where(x => x.ContentTypeId == contentType.Id)
- .Where(x => x.LanguageId == defaultLangId);
+ .Where(x => x.LanguageId == defaultLanguageId);
var sqlDelete = Sql()
.Delete()
.WhereIn(x => x.Id, sqlSelect);
+
Database.Execute(sqlDelete);
//clear out the documentCultureVariation table
@@ -599,10 +708,11 @@ AND umbracoNode.id <> @id",
.From()
.InnerJoin().On(x => x.NodeId, x => x.NodeId)
.Where(x => x.ContentTypeId == contentType.Id)
- .Where(x => x.LanguageId == defaultLangId);
+ .Where(x => x.LanguageId == defaultLanguageId);
sqlDelete = Sql()
.Delete()
.WhereIn(x => x.Id, sqlSelect);
+
Database.Execute(sqlDelete);
//now we need to insert names into these 2 tables based on the invariant data
@@ -610,32 +720,31 @@ AND umbracoNode.id <> @id",
//insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang
var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId);
sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate)
- .Append($", {defaultLangId}") //default language ID
+ .Append($", {defaultLanguageId}") //default language ID
.From()
.InnerJoin().On(x => x.NodeId, x => x.NodeId)
.Where(x => x.ContentTypeId == contentType.Id);
var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
+
Database.Execute(sqlInsert);
//insert rows into the documentCultureVariation table
cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId);
sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published)
.AndSelect(x => x.Text)
- .Append($", 1, {defaultLangId}") //make Available + default language ID
+ .Append($", 1, {defaultLanguageId}") //make Available + default language ID
.From()
.InnerJoin().On(x => x.NodeId, x => x.NodeId)
.InnerJoin().On(x => x.NodeId, x => x.NodeId)
.Where(x => x.ContentTypeId == contentType.Id);
sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
+
Database.Execute(sqlInsert);
break;
case ContentVariation.Nothing:
- //move the property data
- MovePropertyDataToVariantNothing(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds);
-
- //we dont need to move the names! this is because we always keep the invariant names with the name of the default language.
+ //we don't need to move the names! this is because we always keep the invariant names with the name of the default language.
//however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :(
// if we want these SQL statements back, look into GIT history
@@ -649,73 +758,102 @@ AND umbracoNode.id <> @id",
}
///
- /// This will move all property data from variant to invariant
+ /// Copies property data from one language to another.
///
- ///
- /// Optional list of property type ids of the properties to be updated
- /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated
- private void MovePropertyDataToVariantNothing(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null)
+ /// The source language (can be null ie invariant).
+ /// The target language (can be null ie invariant)
+ /// The property type identifiers.
+ /// The content type identifiers.
+ private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null)
{
- //first clear out any existing property data that might already exists under the default lang
+ // fixme - should we batch then?
+ var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0);
+ if (whereInArgsCount > 2000)
+ throw new NotSupportedException("Too many property/content types.");
+
+ //first clear out any existing property data that might already exists under the target language
var sqlDelete = Sql()
- .Delete()
- .Where(x => x.LanguageId == null);
- if (sqlPropertyTypeIds != null)
- sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds);
- if (propertyTypeIds != null)
- sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds);
+ .Delete();
+
+ // not ok for SqlCe (no JOIN in DELETE)
+ //if (contentTypeIds != null)
+ // sqlDelete
+ // .From()
+ // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id)
+ // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId);
+
+ Sql inSql = null;
+ if (contentTypeIds != null)
+ {
+ inSql = Sql()
+ .Select(x => x.Id)
+ .From()
+ .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId)
+ .WhereIn(x => x.ContentTypeId, contentTypeIds);
+ sqlDelete.WhereIn(x => x.VersionId, inSql);
+ }
+
+ // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it
+ if (targetLanguageId == null)
+ sqlDelete.Where(x => x.LanguageId == null);
+ else
+ sqlDelete.Where(x => x.LanguageId == targetLanguageId);
+
+ sqlDelete
+ .WhereIn(x => x.PropertyTypeId, propertyTypeIds);
+
+ // see note above, not ok for SqlCe
+ //if (contentTypeIds != null)
+ // sqlDelete
+ // .WhereIn(x => x.ContentTypeId, contentTypeIds);
Database.Execute(sqlDelete);
- //now insert all property data into the default language that exists under the invariant lang
+ //now insert all property data into the target language that exists under the source language
+ var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL";
var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId);
var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue)
- .Append(", NULL") //null language ID
- .From()
- .Where(x => x.LanguageId == defaultLangId);
- if (sqlPropertyTypeIds != null)
- sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds);
- if (propertyTypeIds != null)
- sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds);
+ .Append(", " + targetLanguageIdS) //default language ID
+ .From();
+
+ if (contentTypeIds != null)
+ sqlSelectData
+ .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id)
+ .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId);
+
+ // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it
+ if (sourceLanguageId == null)
+ sqlSelectData.Where(x => x.LanguageId == null);
+ else
+ sqlSelectData.Where(x => x.LanguageId == sourceLanguageId);
+
+ sqlSelectData
+ .WhereIn(x => x.PropertyTypeId, propertyTypeIds);
+
+ if (contentTypeIds != null)
+ sqlSelectData
+ .WhereIn(x => x.ContentTypeId, contentTypeIds);
var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData);
Database.Execute(sqlInsert);
- }
- ///
- /// This will move all property data from invariant to variant
- ///
- ///
- /// Optional list of property type ids of the properties to be updated
- /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated
- private void MovePropertyDataToVariantCulture(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null)
- {
- //first clear out any existing property data that might already exists under the default lang
- var sqlDelete = Sql()
- .Delete()
- .Where(x => x.LanguageId == defaultLangId);
- if (sqlPropertyTypeIds != null)
- sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds);
- if (propertyTypeIds != null)
- sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds);
+ // when copying from Culture, keep the original values around in case we want to go back
+ // when copying from Nothing, kill the original values, we don't want them around
+ if (sourceLanguageId == null)
+ {
+ sqlDelete = Sql()
+ .Delete();
- Database.Execute(sqlDelete);
+ if (contentTypeIds != null)
+ sqlDelete.WhereIn(x => x.VersionId, inSql);
- //now insert all property data into the default language that exists under the invariant lang
- var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId);
- var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue)
- .Append($", {defaultLangId}") //default language ID
- .From()
- .Where(x => x.LanguageId == null);
- if (sqlPropertyTypeIds != null)
- sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds);
- if (propertyTypeIds != null)
- sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds);
-
- var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData);
+ sqlDelete
+ .Where(x => x.LanguageId == null)
+ .WhereIn(x => x.PropertyTypeId, propertyTypeIds);
- Database.Execute(sqlInsert);
+ Database.Execute(sqlDelete);
+ }
}
private void DeletePropertyType(int contentTypeId, int propertyTypeId)
@@ -851,24 +989,6 @@ AND umbracoNode.id <> @id",
}
}
- ///
- public IEnumerable GetTypesDirectlyComposedOf(int id)
- {
- //fixme - this will probably be more efficient to simply load all content types and do the calculation, see GetWhereCompositionIsUsedInContentTypes
-
- var sql = Sql()
- .SelectAll()
- .From()
- .InnerJoin()
- .On(left => left.NodeId, right => right.ChildId)
- .Where(x => x.NodeObjectType == NodeObjectTypeId)
- .Where(x => x.ParentId == id);
- var dtos = Database.Fetch(sql);
- return dtos.Any()
- ? GetMany(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray())
- : Enumerable.Empty();
- }
-
internal static class ContentTypeQueryMapper
{
public class AssociatedTemplate
diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs
index b65ab83439..7c3e835a77 100644
--- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs
+++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs
@@ -1,15 +1,10 @@
using System;
using System.Collections.Concurrent;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Globalization;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
-using System.Text;
using System.Threading;
-using System.Threading.Tasks;
namespace Umbraco.Core.Security
{
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
index 66b3982b49..06ba1ada79 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
@@ -30,12 +30,12 @@ namespace Umbraco.Core.Services
string[] filterPropertyTypes = null)
{
filterContentTypes = filterContentTypes == null
- ? new string[] { }
- : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray();
+ ? Array.Empty()
+ : filterContentTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
filterPropertyTypes = filterPropertyTypes == null
- ? new string[] {}
- : filterPropertyTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray();
+ ? Array.Empty()
+ : filterPropertyTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
//create the full list of property types to use as the filter
//this is the combination of all property type aliases found in the content types passed in for the filter
@@ -47,7 +47,7 @@ namespace Umbraco.Core.Services
.Union(filterPropertyTypes)
.ToArray();
- var sourceId = source != null ? source.Id : 0;
+ var sourceId = source?.Id ?? 0;
// find out if any content type uses this content type
var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray();
@@ -161,6 +161,5 @@ namespace Umbraco.Core.Services
return all;
}
-
}
}
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index ed682096c0..49f3d1a123 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -368,6 +368,11 @@ namespace Umbraco.Core.Services
/// A publishing document is a document with values that are being published, i.e.
/// that have been published or cleared via and
/// .
+ /// When one needs to publish or unpublish a single culture, or all cultures, using
+ /// and is the way to go. But if one needs to, say, publish two cultures and unpublish a third
+ /// one, in one go, then one needs to invoke and
+ /// on the content itself - this prepares the content, but does not commit anything - and then, invoke
+ /// to actually commit the changes to the database.
/// The document is *always* saved, even when publishing fails.
///
PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true);
@@ -375,11 +380,30 @@ namespace Umbraco.Core.Services
///
/// Saves and publishes a document branch.
///
+ ///
+ /// Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more
+ /// that one culture, see the other overload of this method.
+ /// The parameter determines which documents are published. When false,
+ /// only those documents that are already published, are republished. When true, all documents are
+ /// published.
+ ///
IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0);
///
/// Saves and publishes a document branch.
///
+ ///
+ /// The parameter determines which documents are published. When false,
+ /// only those documents that are already published, are republished. When true, all documents are
+ /// published.
+ /// The parameter is a function which determines whether a document has
+ /// values to publish (else there is no need to publish it). If one wants to publish only a selection of
+ /// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
+ /// cultures may trigger an unwanted republish.
+ /// The parameter is a function to execute to publish cultures, on
+ /// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
+ /// whether the cultures could be published.
+ ///
IEnumerable SaveAndPublishBranch(IContent content, bool force, Func editing, Func publishCultures, int userId = 0);
///
diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs
index ac1171cfdd..4025effb21 100644
--- a/src/Umbraco.Core/Services/Implement/ContentService.cs
+++ b/src/Umbraco.Core/Services/Implement/ContentService.cs
@@ -1272,12 +1272,49 @@ namespace Umbraco.Core.Services.Implement
bool IsEditing(IContent c, string l)
=> c.PublishName != c.Name ||
- c.PublishedCultures.Any(x => c.GetCultureName(x) != c.GetPublishName(x)) ||
- c.Properties.Any(x => x.Values.Where(y => culture == "*" || y.Culture == l).Any(y => !y.EditedValue.Equals(y.PublishedValue)));
+ c.PublishedCultures.Where(x => x.InvariantEquals(l)).Any(x => c.GetCultureName(x) != c.GetPublishName(x)) ||
+ c.Properties.Any(x => x.Values.Where(y => culture == "*" || y.Culture.InvariantEquals(l)).Any(y => !y.EditedValue.Equals(y.PublishedValue)));
return SaveAndPublishBranch(content, force, document => IsEditing(document, culture), document => document.PublishCulture(culture), userId);
}
+ // fixme - make this public once we know it works + document
+ private IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0)
+ {
+ // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
+ // and not to == them, else we would be comparing references, and that is a bad thing
+
+ cultures = cultures ?? Array.Empty();
+
+ // determines whether the document is edited, and thus needs to be published,
+ // for the specified cultures (it may be edited for other cultures and that
+ // should not trigger a publish).
+ bool IsEdited(IContent c)
+ {
+ if (cultures.Length == 0)
+ {
+ // nothing = everything
+ return c.PublishName != c.Name ||
+ c.PublishedCultures.Any(x => c.GetCultureName(x) != c.GetPublishName(x)) ||
+ c.Properties.Any(x => x.Values.Any(y => !y.EditedValue.Equals(y.PublishedValue)));
+ }
+
+ return c.PublishName != c.Name ||
+ c.PublishedCultures.Where(x => cultures.Contains(x, StringComparer.InvariantCultureIgnoreCase)).Any(x => c.GetCultureName(x) != c.GetPublishName(x)) ||
+ c.Properties.Any(x => x.Values.Where(y => cultures.Contains(y.Culture, StringComparer.InvariantCultureIgnoreCase)).Any(y => !y.EditedValue.Equals(y.PublishedValue)));
+ }
+
+ // publish the specified cultures
+ bool PublishCultures(IContent c)
+ {
+ return cultures.Length == 0
+ ? c.PublishCulture() // nothing = everything
+ : cultures.All(c.PublishCulture);
+ }
+
+ return SaveAndPublishBranch(content, force, IsEdited, PublishCultures, userId);
+ }
+
///
public IEnumerable SaveAndPublishBranch(IContent document, bool force,
Func editing, Func publishCultures, int userId = 0)
diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
index 60677cfd81..b74abc03f7 100644
--- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
+++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
@@ -344,37 +344,18 @@ namespace Umbraco.Core.Services.Implement
}
}
+ public IEnumerable GetComposedOf(int id, IEnumerable all)
+ {
+ return all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id));
+
+ }
+
public IEnumerable GetComposedOf(int id)
{
- //fixme: this is essentially the same as ContentTypeServiceExtensions.GetWhereCompositionIsUsedInContentTypes which loads
- // all content types to figure this out, this instead makes quite a few queries so should be replaced
-
- using (var scope = ScopeProvider.CreateScope(autoComplete: true))
- {
- scope.ReadLock(ReadLockIds);
-
- // hash set handles duplicates
- var composed = new HashSet(new DelegateEqualityComparer(
- (x, y) => x.Id == y.Id,
- x => x.Id.GetHashCode()));
-
- var ids = new Stack();
- ids.Push(id);
-
- while (ids.Count > 0)
- {
- var i = ids.Pop();
- var result = Repository.GetTypesDirectlyComposedOf(i).ToArray();
-
- foreach (var c in result)
- {
- composed.Add(c);
- ids.Push(c.Id);
- }
- }
-
- return composed.ToArray();
- }
+ // GetAll is cheap, repository has a full dataset cache policy
+ // fixme - still, because it uses the cache, race conditions!
+ var allContentTypes = GetAll(Array.Empty());
+ return GetComposedOf(id, allContentTypes);
}
public int Count()
diff --git a/src/Umbraco.Tests/Composing/ActionCollectionTests.cs b/src/Umbraco.Tests/Composing/ActionCollectionTests.cs
index 04bd0a2e1e..46e4eee765 100644
--- a/src/Umbraco.Tests/Composing/ActionCollectionTests.cs
+++ b/src/Umbraco.Tests/Composing/ActionCollectionTests.cs
@@ -49,6 +49,8 @@ namespace Umbraco.Tests.Composing
public bool ShowInNotifier => false;
public bool CanBePermissionAssigned => true;
+
+ public bool OpensDialog => true;
}
public class NonSingletonAction : IAction
@@ -66,6 +68,8 @@ namespace Umbraco.Tests.Composing
public bool ShowInNotifier => false;
public bool CanBePermissionAssigned => true;
+
+ public bool OpensDialog => true;
}
#endregion
diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs
index f25382d557..f186ae8e83 100644
--- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs
+++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs
@@ -1,10 +1,8 @@
-using System.Runtime.Remoting;
-using NUnit.Framework;
+using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
-using NPoco;
using Umbraco.Core;
using Umbraco.Core.Events;
using Umbraco.Core.Exceptions;
@@ -12,10 +10,9 @@ using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
-using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Tests.Testing;
-using Umbraco.Core.Components;
+using Umbraco.Tests.Scoping;
namespace Umbraco.Tests.Services
{
diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs
new file mode 100644
index 0000000000..c28d4f7955
--- /dev/null
+++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs
@@ -0,0 +1,773 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using LightInject;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Core;
+using Umbraco.Core.Cache;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.PublishedContent;
+using Umbraco.Core.Persistence;
+using Umbraco.Core.Persistence.Dtos;
+using Umbraco.Core.Persistence.Repositories;
+using Umbraco.Core.PropertyEditors;
+using Umbraco.Core.Services;
+using Umbraco.Core.Sync;
+using Umbraco.Tests.Testing;
+using Umbraco.Web.PublishedCache;
+using Umbraco.Web.PublishedCache.NuCache;
+using Umbraco.Web.PublishedCache.NuCache.DataSource;
+using Umbraco.Web.Routing;
+
+namespace Umbraco.Tests.Services
+{
+ [TestFixture]
+ [Apartment(ApartmentState.STA)]
+ [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true)]
+ public class ContentTypeServiceVariantsTests : TestWithSomeContentBase
+ {
+ protected override void Compose()
+ {
+ base.Compose();
+
+ // pfew - see note in ScopedNuCacheTests?
+ Container.RegisterSingleton();
+ Container.RegisterSingleton(f => Mock.Of());
+ Container.RegisterCollectionBuilder()
+ .Add(f => f.TryGetInstance().GetCacheRefreshers());
+ }
+
+ protected override IPublishedSnapshotService CreatePublishedSnapshotService()
+ {
+ var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true };
+ var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor);
+ var runtimeStateMock = new Mock();
+ runtimeStateMock.Setup(x => x.Level).Returns(() => RuntimeLevel.Run);
+
+ var contentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), Mock.Of());
+ //var documentRepository = Mock.Of();
+ var documentRepository = Container.GetInstance();
+ var mediaRepository = Mock.Of();
+ var memberRepository = Mock.Of();
+
+ return new PublishedSnapshotService(
+ options,
+ null,
+ runtimeStateMock.Object,
+ ServiceContext,
+ contentTypeFactory,
+ null,
+ publishedSnapshotAccessor,
+ Mock.Of(),
+ Logger,
+ ScopeProvider,
+ documentRepository, mediaRepository, memberRepository,
+ DefaultCultureAccessor,
+ new DatabaseDataSource(),
+ Container.GetInstance(), new SiteDomainHelper());
+ }
+
+ public class LocalServerMessenger : ServerMessengerBase
+ {
+ public LocalServerMessenger()
+ : base(false)
+ { }
+
+ protected override void DeliverRemote(ICacheRefresher refresher, MessageType messageType, IEnumerable
/// The content and variants to unpublish
///
- [EnsureUserPermissionForContent("model.Id", 'U')]
+ [EnsureUserPermissionForContent("model.Id", 'Z')]
[OutgoingEditorModelEvent]
public ContentItemDisplay PostUnpublish(UnpublishContent model)
{
diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs
index 22d86631ca..563e8dda1a 100644
--- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs
+++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs
@@ -119,66 +119,74 @@ namespace Umbraco.Web.Editors
/// Type of content Type, eg documentType or mediaType
/// Id of composition content type
///
- protected IEnumerable PerformGetWhereCompositionIsUsedInContentTypes(int contentTypeId,
- UmbracoObjectTypes type)
+ protected IEnumerable PerformGetWhereCompositionIsUsedInContentTypes(int contentTypeId, UmbracoObjectTypes type)
{
- IContentTypeComposition source = null;
+ var id = 0;
- //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic
+ if (contentTypeId > 0)
+ {
+ IContentTypeComposition source;
- IContentTypeComposition[] allContentTypes;
+ switch (type)
+ {
+ case UmbracoObjectTypes.DocumentType:
+ source = Services.ContentTypeService.Get(contentTypeId);
+ break;
+
+ case UmbracoObjectTypes.MediaType:
+ source = Services.ContentTypeService.Get(contentTypeId);
+ break;
+
+ case UmbracoObjectTypes.MemberType:
+ source = Services.MemberTypeService.Get(contentTypeId);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(type));
+ }
+
+ if (source == null)
+ throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+
+ id = source.Id;
+ }
+
+ IEnumerable composedOf;
switch (type)
{
case UmbracoObjectTypes.DocumentType:
- if (contentTypeId > 0)
- {
- source = Services.ContentTypeService.Get(contentTypeId);
- if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
- }
- allContentTypes = Services.ContentTypeService.GetAll().Cast().ToArray();
+ composedOf = Services.ContentTypeService.GetComposedOf(id);
break;
case UmbracoObjectTypes.MediaType:
- if (contentTypeId > 0)
- {
- source = Services.ContentTypeService.Get(contentTypeId);
- if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
- }
- allContentTypes = Services.ContentTypeService.GetAll().Cast().ToArray();
+ composedOf = Services.MediaTypeService.GetComposedOf(id);
break;
case UmbracoObjectTypes.MemberType:
- if (contentTypeId > 0)
- {
- source = Services.MemberTypeService.Get(contentTypeId);
- if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
- }
- allContentTypes = Services.MemberTypeService.GetAll().Cast().ToArray();
+ composedOf = Services.MemberTypeService.GetComposedOf(id);
break;
default:
- throw new ArgumentOutOfRangeException("The entity type was not a content type");
+ throw new ArgumentOutOfRangeException(nameof(type));
}
- var contentTypesWhereCompositionIsUsed = source.GetWhereCompositionIsUsedInContentTypes(allContentTypes);
- return contentTypesWhereCompositionIsUsed
- .Select(x => Mapper.Map(x))
- .Select(x =>
- {
- //translate the name
- x.Name = TranslateItem(x.Name);
+ EntityBasic TranslateName(EntityBasic e)
+ {
+ e.Name = TranslateItem(e.Name);
+ return e;
+ }
- return x;
- })
+ return composedOf
+ .Select(Mapper.Map)
+ .Select(TranslateName)
.ToList();
}
+
protected string TranslateItem(string text)
{
if (text == null)
- {
return null;
- }
if (text.StartsWith("#") == false)
return text;
diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs
index fcbe4bdd4c..5444aadcec 100644
--- a/src/Umbraco.Web/Editors/EntityController.cs
+++ b/src/Umbraco.Web/Editors/EntityController.cs
@@ -131,13 +131,10 @@ namespace Umbraco.Web.Editors
if (tree == null) continue; //shouldn't occur
var searchableTreeAttribute = searchableTree.Value.SearchableTree.GetType().GetCustomAttribute(false);
- var treeAttribute = tree.GetTreeAttribute();
- long total;
-
- result[treeAttribute.GetRootNodeDisplayName(Services.TextService)] = new TreeSearchResult
+ result[tree.GetRootNodeDisplayName(Services.TextService)] = new TreeSearchResult
{
- Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out total),
+ Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out var total),
TreeAlias = searchableTree.Key,
AppAlias = searchableTree.Value.AppAlias,
JsFormatterService = searchableTreeAttribute == null ? "" : searchableTreeAttribute.ServiceName,
diff --git a/src/Umbraco.Web/Models/Trees/MenuItem.cs b/src/Umbraco.Web/Models/Trees/MenuItem.cs
index 88d772b939..412cd9106d 100644
--- a/src/Umbraco.Web/Models/Trees/MenuItem.cs
+++ b/src/Umbraco.Web/Models/Trees/MenuItem.cs
@@ -38,6 +38,7 @@ namespace Umbraco.Web.Models.Trees
SeperatorBefore = false;
Icon = legacyMenu.Icon;
Action = legacyMenu;
+ OpensDialog = legacyMenu.OpensDialog;
}
#endregion
@@ -71,6 +72,10 @@ namespace Umbraco.Web.Models.Trees
[DataMember(Name = "cssclass")]
public string Icon { get; set; }
+
+ [DataMember(Name = "opensDialog")]
+ public bool OpensDialog { get; set; }
+
#endregion
#region Constants
diff --git a/src/Umbraco.Web/Models/Trees/SectionRootNode.cs b/src/Umbraco.Web/Models/Trees/SectionRootNode.cs
index cd4fc3e483..730f6e2962 100644
--- a/src/Umbraco.Web/Models/Trees/SectionRootNode.cs
+++ b/src/Umbraco.Web/Models/Trees/SectionRootNode.cs
@@ -28,6 +28,7 @@ namespace Umbraco.Web.Models.Trees
{
private static readonly string RootId = Core.Constants.System.Root.ToString(CultureInfo.InvariantCulture);
private bool _isGroup;
+ private bool _isSingleNodeTree;
///
/// Creates a group node for grouped multiple trees
@@ -87,13 +88,15 @@ namespace Umbraco.Web.Models.Trees
///
///
///
+ ///
///
- public static TreeRootNode CreateSingleTreeRoot(string nodeId, string getChildNodesUrl, string menuUrl, string title, TreeNodeCollection children)
+ public static TreeRootNode CreateSingleTreeRoot(string nodeId, string getChildNodesUrl, string menuUrl, string title, TreeNodeCollection children, bool isSingleNodeTree = false)
{
return new TreeRootNode(nodeId, getChildNodesUrl, menuUrl)
{
Children = children,
- Name = title
+ Name = title,
+ _isSingleNodeTree = isSingleNodeTree
};
}
@@ -150,6 +153,6 @@ namespace Umbraco.Web.Models.Trees
/// This is used in the UI to configure a full screen section/app
///
[DataMember(Name = "containsTrees")]
- public bool ContainsTrees => Children.Count > 0;
+ public bool ContainsTrees => Children.Count > 0 || !_isSingleNodeTree;
}
}
diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs
index f414702824..671a949a77 100644
--- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs
+++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs
@@ -1160,6 +1160,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
var pdatas = new List();
foreach (var pvalue in prop.Values)
{
+ // sanitize - properties should be ok but ... never knows
+ if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment))
+ continue;
+
// note: at service level, invariant is 'null', but here invariant becomes 'string.Empty'
var value = published ? pvalue.PublishedValue : pvalue.EditedValue;
if (value != null)
@@ -1191,15 +1195,19 @@ namespace Umbraco.Web.PublishedCache.NuCache
var cultureData = new Dictionary();
- var names = content is IContent document
+ // sanitize - names should be ok but ... never knows
+ if (content.GetContentType().VariesByCulture())
+ {
+ var infos = content is IContent document
? (published
? document.PublishCultureInfos
: document.CultureInfos)
: content.CultureInfos;
- foreach (var (culture, name) in names)
- {
- cultureData[culture] = new CultureVariation { Name = name.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue };
+ foreach (var (culture, info) in infos)
+ {
+ cultureData[culture] = new CultureVariation { Name = info.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue };
+ }
}
//the dictionary that will be serialized
diff --git a/src/Umbraco.Web/Services/ApplicationTreeService.cs b/src/Umbraco.Web/Services/ApplicationTreeService.cs
index f13c0547b6..86bfc5d0bb 100644
--- a/src/Umbraco.Web/Services/ApplicationTreeService.cs
+++ b/src/Umbraco.Web/Services/ApplicationTreeService.cs
@@ -20,16 +20,18 @@ namespace Umbraco.Web.Services
{
private readonly ILogger _logger;
private readonly CacheHelper _cache;
+ private readonly TypeLoader _typeLoader;
private Lazy> _allAvailableTrees;
internal const string TreeConfigFileName = "trees.config";
private static string _treeConfig;
private static readonly object Locker = new object();
private readonly Lazy>> _groupedTrees;
- public ApplicationTreeService(ILogger logger, CacheHelper cache)
+ public ApplicationTreeService(ILogger logger, CacheHelper cache, TypeLoader typeLoader)
{
_logger = logger;
_cache = cache;
+ _typeLoader = typeLoader;
_groupedTrees = new Lazy>>(InitGroupedTrees);
}
@@ -443,19 +445,18 @@ namespace Umbraco.Web.Services
///
private class LazyEnumerableTrees : IEnumerable
{
- public LazyEnumerableTrees()
+ public LazyEnumerableTrees(TypeLoader typeLoader)
{
_lazyTrees = new Lazy>(() =>
{
var added = new List();
// Load all Controller Trees by attribute
- var types = Current.TypeLoader.GetTypesWithAttribute(); // fixme inject
+ var types = typeLoader.GetTypesWithAttribute(); // fixme inject
//convert them to ApplicationTree instances
var items = types
- .Select(x =>
- new Tuple(x, x.GetCustomAttributes(false).Single()))
- .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly()))
+ .Select(x => (tree: x, treeAttribute: x.GetCustomAttributes(false).Single()))
+ .Select(x => new ApplicationTree(x.treeAttribute.Initialize, x.treeAttribute.SortOrder, x.treeAttribute.ApplicationAlias, x.treeAttribute.Alias, x.treeAttribute.Title, x.treeAttribute.IconClosed, x.treeAttribute.IconOpen, x.tree.GetFullNameWithAssembly()))
.ToArray();
added.AddRange(items.Select(x => x.Alias));
@@ -465,7 +466,7 @@ namespace Umbraco.Web.Services
}
private readonly Lazy> _lazyTrees;
-
+
///
/// Returns an enumerator that iterates through the collection.
///
diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs
index 273a7afb37..c1192b6909 100644
--- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs
+++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs
@@ -41,7 +41,7 @@ namespace Umbraco.Web.Trees
var groupedTrees = Services.ApplicationTreeService.GetGroupedApplicationTrees(application, onlyInitialized);
var allTrees = groupedTrees.Values.SelectMany(x => x).ToList();
- if (string.IsNullOrEmpty(tree) == false || allTrees.Count <= 1)
+ if (string.IsNullOrEmpty(tree) == false || allTrees.Count == 1)
{
var apptree = !tree.IsNullOrWhiteSpace()
? allTrees.FirstOrDefault(x => x.Alias == tree)
@@ -171,12 +171,15 @@ namespace Umbraco.Web.Trees
throw new InvalidOperationException("Could not create root node for tree " + configTree.Alias);
}
+ var treeAttribute = configTree.GetTreeAttribute();
+
var sectionRoot = TreeRootNode.CreateSingleTreeRoot(
rootId,
rootNode.Result.ChildNodesUrl,
rootNode.Result.MenuUrl,
rootNode.Result.Name,
- byControllerAttempt.Result);
+ byControllerAttempt.Result,
+ treeAttribute.IsSingleNodeTree);
//assign the route path based on the root node, this means it will route there when the section is navigated to
//and no dashboards will be available for this section
diff --git a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs
index 171601a338..c688491ebb 100644
--- a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs
+++ b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs
@@ -48,28 +48,6 @@ namespace Umbraco.Web.Trees
return tree.GetRuntimeType().GetTreeAttribute();
}
- internal static string GetRootNodeDisplayName(this TreeAttribute attribute, ILocalizedTextService textService)
- {
- var label = $"[{attribute.Alias}]";
-
- // try to look up a the localized tree header matching the tree alias
- var localizedLabel = textService.Localize("treeHeaders/" + attribute.Alias);
-
- // if the localizedLabel returns [alias] then return the title attribute from the trees.config file, if it's defined
- if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase))
- {
- if (string.IsNullOrEmpty(attribute.Title) == false)
- label = attribute.Title;
- }
- else
- {
- // the localizedLabel translated into something that's not just [alias], so use the translation
- label = localizedLabel;
- }
-
- return label;
- }
-
internal static Attempt TryGetControllerTree(this ApplicationTree appTree)
{
//get reference to all TreeApiControllers
diff --git a/src/Umbraco.Web/Trees/DictionaryTreeController.cs b/src/Umbraco.Web/Trees/DictionaryTreeController.cs
index ca9a54f873..3043377d65 100644
--- a/src/Umbraco.Web/Trees/DictionaryTreeController.cs
+++ b/src/Umbraco.Web/Trees/DictionaryTreeController.cs
@@ -13,7 +13,7 @@ namespace Umbraco.Web.Trees
[UmbracoTreeAuthorize(Constants.Trees.Dictionary)]
[Mvc.PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
- [Tree(Constants.Applications.Translation, Constants.Trees.Dictionary, null, sortOrder: 0)]
+ [Tree(Constants.Applications.Translation, Constants.Trees.Dictionary, null)]
public class DictionaryTreeController : TreeController
{
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)
diff --git a/src/Umbraco.Web/Trees/TreeAttribute.cs b/src/Umbraco.Web/Trees/TreeAttribute.cs
index 5df0275298..b214698721 100644
--- a/src/Umbraco.Web/Trees/TreeAttribute.cs
+++ b/src/Umbraco.Web/Trees/TreeAttribute.cs
@@ -28,13 +28,15 @@ namespace Umbraco.Web.Trees
/// The icon open.
/// if set to true [initialize].
/// The sort order.
+ /// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app)
public TreeAttribute(string appAlias,
string alias,
string title,
string iconClosed = "icon-folder",
string iconOpen = "icon-folder-open",
bool initialize = true,
- int sortOrder = 0)
+ int sortOrder = 0,
+ bool isSingleNodeTree = false)
{
ApplicationAlias = appAlias;
Alias = alias;
@@ -43,6 +45,7 @@ namespace Umbraco.Web.Trees
IconOpen = iconOpen;
Initialize = initialize;
SortOrder = sortOrder;
+ IsSingleNodeTree = isSingleNodeTree;
}
@@ -54,5 +57,10 @@ namespace Umbraco.Web.Trees
public string IconOpen { get; private set; }
public bool Initialize { get; private set; }
public int SortOrder { get; private set; }
+
+ ///
+ /// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app)
+ ///
+ public bool IsSingleNodeTree { get; private set; }
}
}
diff --git a/src/Umbraco.Web/Trees/TreeController.cs b/src/Umbraco.Web/Trees/TreeController.cs
index b53ad8a057..b5708ff57d 100644
--- a/src/Umbraco.Web/Trees/TreeController.cs
+++ b/src/Umbraco.Web/Trees/TreeController.cs
@@ -10,6 +10,7 @@ namespace Umbraco.Web.Trees
public abstract class TreeController : TreeControllerBase
{
private TreeAttribute _attribute;
+ private string _rootNodeDisplayName;
protected TreeController()
{
@@ -20,9 +21,9 @@ namespace Umbraco.Web.Trees
/// The name to display on the root node
///
public override string RootNodeDisplayName
- {
- get { return _attribute.GetRootNodeDisplayName(Services.TextService); }
- }
+ => _rootNodeDisplayName
+ ?? (_rootNodeDisplayName = Services.ApplicationTreeService.GetByAlias(_attribute.Alias)
+ ?.GetRootNodeDisplayName(Services.TextService));
///
/// Gets the current tree alias from the attribute assigned to it.
diff --git a/src/Umbraco.Web/Trees/UserTreeController.cs b/src/Umbraco.Web/Trees/UserTreeController.cs
index e6bd53ddf8..8ae5b002c6 100644
--- a/src/Umbraco.Web/Trees/UserTreeController.cs
+++ b/src/Umbraco.Web/Trees/UserTreeController.cs
@@ -10,7 +10,7 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Users)]
- [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 0)]
+ [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 0, isSingleNodeTree: true)]
[PluginController("UmbracoTrees")]
[CoreTree]
public class UserTreeController : TreeController
diff --git a/src/Umbraco.Web/_Legacy/Actions/Action.cs b/src/Umbraco.Web/_Legacy/Actions/Action.cs
index 388a5735fd..241218ddb7 100644
--- a/src/Umbraco.Web/_Legacy/Actions/Action.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/Action.cs
@@ -174,6 +174,7 @@ namespace Umbraco.Web._Legacy.Actions
public string Alias { get; set; }
public string JsFunctionName { get; set; }
public string JsSource { get; set; }
+ public bool OpensDialog { get; set; }
public PlaceboAction() { }
public PlaceboAction(IAction legacyAction)
@@ -185,6 +186,7 @@ namespace Umbraco.Web._Legacy.Actions
Alias = legacyAction.Alias;
JsFunctionName = legacyAction.JsFunctionName;
JsSource = legacyAction.JsSource;
+ OpensDialog = legacyAction.OpensDialog;
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionAssignDomain.cs b/src/Umbraco.Web/_Legacy/Actions/ActionAssignDomain.cs
index 37de1f8e0f..c313f282ad 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionAssignDomain.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionAssignDomain.cs
@@ -69,6 +69,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionBrowse.cs b/src/Umbraco.Web/_Legacy/Actions/ActionBrowse.cs
index 1425b27917..20dc331516 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionBrowse.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionBrowse.cs
@@ -60,6 +60,8 @@ namespace Umbraco.Web._Legacy.Actions
get { return ""; }
}
+ public bool OpensDialog => false;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionChangeDocType.cs b/src/Umbraco.Web/_Legacy/Actions/ActionChangeDocType.cs
index 9c31c172ab..b68627c38c 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionChangeDocType.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionChangeDocType.cs
@@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionCopy.cs b/src/Umbraco.Web/_Legacy/Actions/ActionCopy.cs
index a489f1d280..5addcec99f 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionCopy.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionCopy.cs
@@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionCreateBlueprintFromContent.cs b/src/Umbraco.Web/_Legacy/Actions/ActionCreateBlueprintFromContent.cs
index e00de39aea..0d028c35b4 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionCreateBlueprintFromContent.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionCreateBlueprintFromContent.cs
@@ -20,6 +20,7 @@ namespace Umbraco.Web._Legacy.Actions
public string Alias { get; private set; }
public string JsFunctionName { get; private set; }
public string JsSource { get; private set; }
+ public bool OpensDialog => true;
public ActionCreateBlueprintFromContent()
{
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionDelete.cs b/src/Umbraco.Web/_Legacy/Actions/ActionDelete.cs
index 09ce4d8602..53f7822d47 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionDelete.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionDelete.cs
@@ -77,6 +77,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionEmptyTranscan.cs b/src/Umbraco.Web/_Legacy/Actions/ActionEmptyTranscan.cs
index 7f8dd6b03c..f0da5323b9 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionEmptyTranscan.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionEmptyTranscan.cs
@@ -73,6 +73,8 @@ namespace Umbraco.Web._Legacy.Actions
}
}
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionExport.cs b/src/Umbraco.Web/_Legacy/Actions/ActionExport.cs
index df78026ea0..56b98c02f2 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionExport.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionExport.cs
@@ -71,6 +71,8 @@ namespace Umbraco.Web._Legacy.Actions
}
}
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionImport.cs b/src/Umbraco.Web/_Legacy/Actions/ActionImport.cs
index 42947cf36e..52f163ee6b 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionImport.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionImport.cs
@@ -72,6 +72,8 @@
}
}
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionMove.cs b/src/Umbraco.Web/_Legacy/Actions/ActionMove.cs
index 80aff5736a..81d1803679 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionMove.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionMove.cs
@@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionNew.cs b/src/Umbraco.Web/_Legacy/Actions/ActionNew.cs
index 72e863e38b..ef1b61efc5 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionNew.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionNew.cs
@@ -70,6 +70,8 @@ namespace Umbraco.Web._Legacy.Actions
}
}
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionNotify.cs b/src/Umbraco.Web/_Legacy/Actions/ActionNotify.cs
index ef281eecbe..fd6bc3d61a 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionNotify.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionNotify.cs
@@ -76,6 +76,9 @@ namespace Umbraco.Web._Legacy.Actions
return false;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionNull.cs b/src/Umbraco.Web/_Legacy/Actions/ActionNull.cs
index 78c5175fb6..3344560c3f 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionNull.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionNull.cs
@@ -51,6 +51,8 @@
get { return string.Empty; }
}
+ public bool OpensDialog => false;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionPackage.cs b/src/Umbraco.Web/_Legacy/Actions/ActionPackage.cs
index 832e691b48..fa17b87073 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionPackage.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionPackage.cs
@@ -75,6 +75,8 @@ namespace Umbraco.Web._Legacy.Actions
}
}
+ public bool OpensDialog => false;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionPackageCreate.cs b/src/Umbraco.Web/_Legacy/Actions/ActionPackageCreate.cs
index f0ccb03d8e..fdec43e810 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionPackageCreate.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionPackageCreate.cs
@@ -75,6 +75,8 @@ namespace Umbraco.Web._Legacy.Actions
}
}
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionProtect.cs b/src/Umbraco.Web/_Legacy/Actions/ActionProtect.cs
index 357dfe89a4..65e9d7128e 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionProtect.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionProtect.cs
@@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionPublish.cs b/src/Umbraco.Web/_Legacy/Actions/ActionPublish.cs
index 6b54873c43..70c7735572 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionPublish.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionPublish.cs
@@ -77,6 +77,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRePublish.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRePublish.cs
index b78af779e4..312ae80825 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionRePublish.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionRePublish.cs
@@ -74,6 +74,9 @@ namespace Umbraco.Web._Legacy.Actions
return false;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRefresh.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRefresh.cs
index 07133b4030..0abf4fcac5 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionRefresh.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionRefresh.cs
@@ -81,6 +81,9 @@ namespace Umbraco.Web._Legacy.Actions
return false;
}
}
+
+ public bool OpensDialog => false;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRestore.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRestore.cs
index da70eb1409..2a2baac070 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionRestore.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionRestore.cs
@@ -27,6 +27,8 @@
public bool CanBePermissionAssigned => false;
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRights.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRights.cs
index e1ee74e61c..beb3b06ddf 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionRights.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionRights.cs
@@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRollback.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRollback.cs
index 59044666f7..3179dc9fb5 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionRollback.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionRollback.cs
@@ -82,6 +82,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionSort.cs b/src/Umbraco.Web/_Legacy/Actions/ActionSort.cs
index b813dcbc8c..48f6b8d1e9 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionSort.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionSort.cs
@@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionToPublish.cs b/src/Umbraco.Web/_Legacy/Actions/ActionToPublish.cs
index ff471bc198..a04a24f4a3 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionToPublish.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionToPublish.cs
@@ -78,6 +78,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => false;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionTranslate.cs b/src/Umbraco.Web/_Legacy/Actions/ActionTranslate.cs
index 0cc5120fd0..157fd827a6 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionTranslate.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionTranslate.cs
@@ -77,6 +77,8 @@ namespace Umbraco.Web._Legacy.Actions
}
}
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs b/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs
index 93d1da2046..11225add1d 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs
@@ -1,13 +1,14 @@
using System;
+using Umbraco.Core;
+using Umbraco.Core.CodeAnnotations;
namespace Umbraco.Web._Legacy.Actions
{
-
-
///
/// This action is invoked when a document is being unpublished
///
+ [ActionMetadata(Constants.Conventions.PermissionCategories.ContentCategory)]
public class ActionUnpublish : IAction
{
//create singleton
@@ -15,68 +16,17 @@ namespace Umbraco.Web._Legacy.Actions
private static readonly ActionUnpublish m_instance = new ActionUnpublish();
#pragma warning restore 612,618
- public static ActionUnpublish Instance
- {
- get { return m_instance; }
- }
+ public static ActionUnpublish Instance => m_instance;
- #region IAction Members
+ public char Letter => 'Z';
+ public string JsFunctionName => "";
+ public string JsSource => null;
+ public string Alias => "unpublish";
+ public string Icon => "circle-dotted";
+ public bool ShowInNotifier => false;
+ public bool CanBePermissionAssigned => true;
+ public bool OpensDialog => false;
- public char Letter
- {
- get
- {
- return 'Z';
- }
- }
-
- public string JsFunctionName
- {
- get
- {
- return "";
- }
- }
-
- public string JsSource
- {
- get
- {
- return null;
- }
- }
-
- public string Alias
- {
- get
- {
- return "unpublish";
- }
- }
-
- public string Icon
- {
- get
- {
- return "circle-dotted";
- }
- }
-
- public bool ShowInNotifier
- {
- get
- {
- return false;
- }
- }
- public bool CanBePermissionAssigned
- {
- get
- {
- return false;
- }
- }
- #endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionUpdate.cs b/src/Umbraco.Web/_Legacy/Actions/ActionUpdate.cs
index 15458e83ad..5621d505a9 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ActionUpdate.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ActionUpdate.cs
@@ -77,6 +77,9 @@ namespace Umbraco.Web._Legacy.Actions
return true;
}
}
+
+ public bool OpensDialog => false;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/ContextMenuSeperator.cs b/src/Umbraco.Web/_Legacy/Actions/ContextMenuSeperator.cs
index 2c66932a04..45a1d0e1c5 100644
--- a/src/Umbraco.Web/_Legacy/Actions/ContextMenuSeperator.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/ContextMenuSeperator.cs
@@ -46,6 +46,8 @@
get { return false; }
}
+ public bool OpensDialog => false;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/_Legacy/Actions/IAction.cs b/src/Umbraco.Web/_Legacy/Actions/IAction.cs
index 410a407517..48a752e7da 100644
--- a/src/Umbraco.Web/_Legacy/Actions/IAction.cs
+++ b/src/Umbraco.Web/_Legacy/Actions/IAction.cs
@@ -14,5 +14,9 @@ namespace Umbraco.Web._Legacy.Actions
/// A path to a supporting JavaScript file for the IAction. A script tag will be rendered out with the reference to the JavaScript file.
///
string JsSource { get; }
+ ///
+ /// Whether or not the action opens a dialog when invoked
+ ///
+ bool OpensDialog { get; }
}
}
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs
index cf39b17e55..5526a3d9a3 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs
@@ -78,6 +78,8 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu
get { return "javascript:actionDeleteRelationType(UmbClientMgr.mainTree().getActionNode().nodeId,UmbClientMgr.mainTree().getActionNode().nodeName);"; }
}
+ public bool OpensDialog => true;
+
#endregion
}
}
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs
index 6018539983..cb776d5246 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs
@@ -78,6 +78,8 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu
get { return "javascript:actionNewRelationType();"; }
}
+ public bool OpensDialog => true;
+
#endregion
}
}